{
  "authors": [
    {
      "name": "Mikey Nichols",
      "url": "https://mnix.dev"
    }
  ],
  "description": "Practical web development, real projects, and lessons from the build.",
  "feed_url": "https://mnixblog.netlify.app/feed.json",
  "home_page_url": "https://mnixblog.netlify.app/",
  "items": [
    {
      "content_html": "<p>For ten articles, the only thing that has ever run our game is <code>main()</code>, in Rust, in a terminal.</p>\n<p>Today, a browser runs it.</p>\n<p>Not a rewrite. Not a port. The same library, with the same twenty-eight tests, compiled for a different machine. The browser gets a handful of operations and a save that is already text. It does not get the rules.</p>\n<p>By the end, a web page shows this:</p>\n<pre><code class=\"language-text\">One hour away.\nStage 1 -&gt; 128  Level 1 -&gt; 47  Gold 0 -&gt; 16637\n</code></pre>\n<p>Stage 128 and level 47 are the numbers <code>tests/idle_hour.rs</code> has been checking since the ninth article. Same engine. Same hour. The call came from JavaScript.</p>\n<p>Along the way we meet WebAssembly, two tools that do the glue work, and one new rule about what is allowed to cross a border.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>This series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.</p>\n<p>If you are only reading along, skip past this code to \"A second machine.\"</p>\n<p>If you are building along, you need the library as it stood at the end of the last article: a project called <code>rustbound-idle</code> with seven source files and two outside tests. (If you kept your project, you already have all of this. The <code>article_10</code> branch of the companion repository has it too.)</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n├── Cargo.lock\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   ├── game.rs\n│   └── save.rs\n└── tests/\n    ├── idle_hour.rs\n    └── save_and_restore.rs\n</code></pre>\n<p><code>Cargo.toml</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">Cargo.toml</figcaption><pre><code class=\"language-toml\">[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n</code></pre></figure>\n<details>\n<summary><code>src/lib.rs</code> · 10 lines</summary>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use save::LoadError;\n</code></pre>\n</details>\n<details>\n<summary><code>src/main.rs</code> · 71 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                game.player.xp_needed()\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    game.player.click_damage()\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                game.player.click_damage(),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let save = game.save_json();\n    println!(\"Saved: {save}\");\n\n    drop(game);\n\n    let mut game = match Game::from_saved_json(&amp;save) {\n        Ok(game) =&gt; game,\n        Err(error) =&gt; {\n            println!(\"Could not load the save: {error:?}\");\n            return;\n        }\n    };\n\n    println!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/rules.rs</code> · 105 lines</summary>\n<pre><code class=\"language-rust\">pub(crate) fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\npub(crate) fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\npub(crate) fn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\npub(crate) fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\npub(crate) fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\npub(crate) fn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\npub(crate) fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\npub(crate) fn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\npub(crate) fn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\npub(crate) fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\npub(crate) fn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/enemy.rs</code> · 19 lines</summary>\n<pre><code class=\"language-rust\">use crate::rules::{enemy_health_for_stage, gold_reward_for_stage, xp_reward_for_stage};\n\npub struct Enemy {\n    pub name: String,\n    pub health: u32,\n    pub(crate) gold_reward: u32,\n    pub(crate) xp_reward: u32,\n}\n\nimpl Enemy {\n    pub(crate) fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/player.rs</code> · 114 lines</summary>\n<pre><code class=\"language-rust\">use crate::rules::{\n    base_damage_for_level, click_damage, click_upgrade_bonus, click_upgrade_cost, xp_to_next_level,\n};\n\npub struct Player {\n    pub gold: u32,\n    pub xp: u32,\n    pub level: u32,\n    pub click_upgrade_level: u32,\n}\n\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    pub(crate) fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    pub(crate) fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    pub(crate) fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= self.xp_needed() {\n            self.xp -= self.xp_needed();\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    pub fn xp_needed(&amp;self) -&gt; u32 {\n        xp_to_next_level(self.level)\n    }\n\n    pub fn click_damage(&amp;self) -&gt; u32 {\n        click_damage(self.base_damage(), self.bonus_damage())\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    pub(crate) fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/game.rs</code> · 158 lines</summary>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{health_after_hit, idle_damage_for_level, is_defeated};\n\npub struct Game {\n    pub stage: u32,\n    pub player: Player,\n    pub enemy: Enemy,\n}\n\nimpl Game {\n    pub fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    pub fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    pub fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nimpl Default for Game {\n    fn default() -&gt; Game {\n        Game::new()\n    }\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    enemy.health = health_after_hit(enemy.health, player.click_damage());\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(long.enemy.health, short.enemy.health);\n        assert_eq!(long.player.gold, short.player.gold);\n        assert_eq!(long.player.xp, short.player.xp);\n        assert_eq!(long.player.level, short.player.level);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/save.rs</code> · 130 lines</summary>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::game::Game;\nuse serde::{Deserialize, Serialize};\n\nconst SAVE_VERSION: u32 = 1;\n\n#[derive(Debug, PartialEq)]\npub enum LoadError {\n    Unreadable,\n    UnsupportedVersion(u32),\n}\n\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct SaveData {\n    save_version: u32,\n    stage: u32,\n    player: PlayerSave,\n    enemy_health: u32,\n}\n\nimpl Game {\n    pub fn save_json(&amp;self) -&gt; String {\n        let save = SaveData {\n            save_version: SAVE_VERSION,\n            stage: self.stage,\n            player: PlayerSave {\n                gold: self.player.gold,\n                xp: self.player.xp,\n                level: self.player.level,\n                click_upgrade_level: self.player.click_upgrade_level,\n            },\n            enemy_health: self.enemy.health,\n        };\n\n        serde_json::to_string(&amp;save).expect(\"a save made of numbers should always serialize\")\n    }\n\n    pub fn from_saved_json(json: &amp;str) -&gt; Result&lt;Game, LoadError&gt; {\n        let save: SaveData = match serde_json::from_str(json) {\n            Ok(save) =&gt; save,\n            Err(_) =&gt; return Err(LoadError::Unreadable),\n        };\n\n        if save.save_version != SAVE_VERSION {\n            return Err(LoadError::UnsupportedVersion(save.save_version));\n        }\n\n        let mut game = Game::new();\n\n        game.stage = save.stage;\n        game.player.gold = save.player.gold;\n        game.player.xp = save.player.xp;\n        game.player.level = save.player.level;\n        game.player.click_upgrade_level = save.player.click_upgrade_level;\n\n        game.enemy = Enemy::for_stage(save.stage);\n        game.enemy.health = save.enemy_health;\n\n        Ok(game)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn a_new_game_saves_its_starting_numbers() {\n        let json = Game::new().save_json();\n\n        assert_eq!(\n            json,\n            r#\"{\"save_version\":1,\"stage\":1,\"player\":{\"gold\":0,\"xp\":0,\"level\":1,\"click_upgrade_level\":0},\"enemy_health\":20}\"#\n        );\n    }\n\n    #[test]\n    fn a_loaded_game_rebuilds_the_enemy_from_the_rules() {\n        let mut game = Game::new();\n        game.tick(600);\n\n        let restored = Game::from_saved_json(&amp;game.save_json()).expect(\"the save should load\");\n\n        assert_eq!(restored.stage, game.stage);\n        assert_eq!(restored.enemy.health, game.enemy.health);\n        assert_eq!(restored.enemy.gold_reward, game.enemy.gold_reward);\n        assert_eq!(restored.enemy.xp_reward, game.enemy.xp_reward);\n    }\n\n    #[test]\n    fn nonsense_is_not_a_save() {\n        assert!(matches!(\n            Game::from_saved_json(\"definitely not a save\"),\n            Err(LoadError::Unreadable)\n        ));\n    }\n\n    #[test]\n    fn a_save_with_missing_pieces_is_not_a_save() {\n        let json = r#\"{\"save_version\":1,\"stage\":4}\"#;\n\n        assert!(matches!(\n            Game::from_saved_json(json),\n            Err(LoadError::Unreadable)\n        ));\n    }\n\n    #[test]\n    fn a_save_from_another_version_is_refused() {\n        let json = r#\"{\n            \"save_version\": 99,\n            \"stage\": 1,\n            \"player\": { \"gold\": 0, \"xp\": 0, \"level\": 1, \"click_upgrade_level\": 0 },\n            \"enemy_health\": 20\n        }\"#;\n\n        assert!(matches!(\n            Game::from_saved_json(json),\n            Err(LoadError::UnsupportedVersion(99))\n        ));\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>tests/idle_hour.rs</code> · 11 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn an_hour_away_moves_the_game_forward() {\n    let mut game = Game::new();\n\n    game.tick(3600);\n\n    assert_eq!(game.stage, 128);\n    assert_eq!(game.player.level, 47);\n}\n</code></pre>\n</details>\n<details>\n<summary><code>tests/save_and_restore.rs</code> · 18 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn a_restored_game_plays_the_same_hour() {\n    let mut original = Game::new();\n    original.tick(600);\n\n    let mut restored = Game::from_saved_json(&amp;original.save_json()).expect(\"the save should load\");\n\n    original.tick(3600);\n    restored.tick(3600);\n\n    assert_eq!(restored.stage, original.stage);\n    assert_eq!(restored.enemy.health, original.enemy.health);\n    assert_eq!(restored.player.gold, original.player.gold);\n    assert_eq!(restored.player.xp, original.player.xp);\n    assert_eq!(restored.player.level, original.player.level);\n}\n</code></pre>\n</details>\n<p>Run <code>cargo test</code>, and twenty-eight tests pass. Run <code>cargo run</code>, and the player clears three stages, saves, loads, and spends an hour away.</p>\n<hr />\n<h2 id=\"a-second-machine\">A second machine<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#a-second-machine\" aria-label=\"Link to this section\">#</a></h2>\n<p>Everything we have compiled so far has been for the computer in front of you. <code>cargo build</code> asks Rust to produce a program for <em>this</em> machine: this processor, this operating system.</p>\n<p>Rust can produce programs for other machines too. A phone. A server with a different processor. Each one is called a <strong>target</strong>, and you pick it when you build.</p>\n<p>WebAssembly is one of those targets.</p>\n<p>It is not a real machine. It is a description of one, agreed on by the people who make browsers, so that every browser can run the same compiled code. Compile Rust for it, and the result is a file a browser can load and run, the way it loads and runs JavaScript.</p>\n<p>The target has a long name:</p>\n<pre><code class=\"language-text\">wasm32-unknown-unknown\n</code></pre>\n<p>The three parts are the processor, the vendor, and the operating system. The processor is 32-bit WebAssembly. The other two are <em>unknown</em>, and they mean it.</p>\n<p>There is no operating system. No files, no clock, no terminal, no <code>println!</code>. A program compiled for this target can do arithmetic and talk to whoever loaded it, and that is all.</p>\n<p>That should sound like a problem.</p>\n<p>It is not, and the reason is the wall we built two articles ago. Our library has never opened a file, never asked the time, never printed. It takes numbers and text, and it gives back numbers and text. <code>main.rs</code> does the printing, and <code>main.rs</code> is not coming with us.</p>\n<p>Install the target:</p>\n<pre><code class=\"language-bash\">rustup target add wasm32-unknown-unknown\n</code></pre>\n<p>That downloads Rust's standard library, compiled for WebAssembly, so that <code>String</code> and <code>Vec</code> and <code>saturating_add</code> all exist on the other side. It takes a moment, and it only happens once.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>main.rs</code> stay behind when we compile the library for the browser?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>WebAssembly can only compile files named <code>lib.rs</code></li>\n<li><code>main.rs</code> prints to a terminal, and this browser target has no terminal</li>\n<li>JavaScript automatically replaces every Rust <code>main()</code> function</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>main.rs</code> prints to a terminal, and this browser target has no terminal</p>\n<p>The engine only calculates with numbers and text. The terminal-specific printing stays in <code>main.rs</code> on the machine that has a terminal.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"two-tools\">Two tools<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#two-tools\" aria-label=\"Link to this section\">#</a></h2>\n<p>WebAssembly has one more limitation that matters to us.</p>\n<p>At the raw WebAssembly boundary we are using today, values cross as WebAssembly's small built-in types. A Rust <code>String</code> or struct cannot cross as a JavaScript string or object by itself.</p>\n<p>Our front door takes a <code>&amp;str</code> and returns a <code>String</code>. Those are not numbers.</p>\n<p>Someone has to write the code that turns a Rust <code>String</code> into a JavaScript string, one byte at a time, on both sides of the line. That code is tedious, and it is the same every time, so a tool writes it. Two tools, in fact:</p>\n<ul>\n<li><strong>wasm-bindgen</strong> is a Rust crate and a command-line program. The crate gives us an attribute to mark what JavaScript should be able to call. The program reads a compiled <code>.wasm</code> file and writes the JavaScript glue for everything marked.</li>\n<li><strong>wasm-pack</strong> runs the whole build: <code>cargo build</code> for the WebAssembly target, then <code>wasm-bindgen</code>, then a shrinking pass, and puts the results in one folder, ready for a web page.</li>\n</ul>\n<p>Install <code>wasm-pack</code>:</p>\n<pre><code class=\"language-bash\">cargo install wasm-pack\n</code></pre>\n<p><code>cargo install</code> builds a program from crates.io and puts it on your path. This one takes a couple of minutes.</p>\n<p>The other tool comes in through <code>Cargo.toml</code>, the way <code>serde</code> did. Open it and make two changes:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">Cargo.toml</figcaption><pre><code class=\"language-toml\" data-highlight-lines=\"6-7,12\">[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nwasm-bindgen = \"0.2\"\n</code></pre></figure>\n<p>The dependency is the familiar part.</p>\n<p>The <code>[lib]</code> section is new. It tells Cargo what <em>shape</em> to build the library in.</p>\n<p><code>rlib</code> is the shape Rust uses: it is what <code>main.rs</code> and the tests have been linking against all along. <code>cdylib</code> is a shape other languages can load. Think of a front door for the family and a side door for guests. We ask for both, so that <code>cargo run</code> and <code>cargo test</code> keep using the door they know, and <code>wasm-pack</code> gets one it can open.</p>\n<p>Build:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>Cargo downloads <code>wasm-bindgen</code> and a few helpers, compiles them, and finishes with nothing to say. Nothing is marked yet.</p>\n<p>Run <code>cargo test</code> too. Twenty-eight.</p>\n<hr />\n<h2 id=\"do-not-export-the-engine\">Do not export the engine<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#do-not-export-the-engine\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the tempting move: put the <code>wasm-bindgen</code> attribute on <code>Game</code>, on <code>Player</code>, on <code>Enemy</code>, and let JavaScript have everything.</p>\n<p>We are not going to do that, for the same reason <code>rules</code> is not on the front door.</p>\n<p><code>Game</code> has never heard of a browser. The moment it carries a browser attribute, it has. Every future change to the engine would have to ask whether JavaScript still likes the shape. And JavaScript would be free to reach past the methods, read the fields, and start doing arithmetic of its own.</p>\n<p>So we build an <strong>adapter</strong>: a small type that holds a <code>Game</code>, and shows JavaScript only what we choose.</p>\n<p>It is a separate concern, so it gets a separate module. Create <code>src/web.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">use crate::game::Game;\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen(js_name = Game)]\npub struct WebGame {\n    inner: Game,\n}\n\n#[wasm_bindgen(js_class = Game)]\nimpl WebGame {\n    #[wasm_bindgen(constructor)]\n    pub fn new() -&gt; WebGame {\n        WebGame { inner: Game::new() }\n    }\n\n    pub fn click(&amp;mut self) {\n        self.inner.click();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        self.inner.tick(seconds);\n    }\n}\n</code></pre></figure>\n<p>And register it. In <code>src/lib.rs</code>, add <code>mod web;</code> under <code>mod save;</code>. It does not go on the front door. JavaScript will not come in through <code>lib.rs</code>; it comes in through the glue.</p>\n<p>Now read <code>web.rs</code> from the top.</p>\n<p><code>use wasm_bindgen::prelude::*;</code> brings in everything the crate expects us to use. A <em>prelude</em> is a crate's \"you will want all of these\" list. The <code>*</code> means all of it.</p>\n<p><code>#[wasm_bindgen]</code> is an attribute, like <code>#[derive(...)]</code> and <code>#[test]</code>: a note to the compiler, attached to the thing below it. This one says <em>JavaScript should be able to see this</em>.</p>\n<p><code>WebGame</code> is the adapter. It has one field, <code>inner</code>, holding the real <code>Game</code>. The field is private. JavaScript cannot see inside, and neither can anything outside this file.</p>\n<p><code>js_name = Game</code> on the struct, and <code>js_class = Game</code> on the <code>impl</code> block, say what the type is called <em>on the JavaScript side</em>. In Rust it is <code>WebGame</code>, so the two never collide. In the browser it is <code>Game</code>, because that is what it is.</p>\n<p><code>#[wasm_bindgen(constructor)]</code> marks <code>new()</code> as the thing <code>new Game()</code> runs in JavaScript.</p>\n<p><code>click()</code> and <code>tick()</code> are the smallest methods in the project. They hand the call to <code>inner</code> and that is all. That is what an adapter does. The rules stay on the far side.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>WebGame</code> hold a private <code>Game</code> instead of exporting every engine field?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Private fields make WebAssembly download faster</li>\n<li>The adapter keeps JavaScript's interface small and leaves the rules in Rust</li>\n<li>Rust structs cannot contain public fields</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The adapter keeps JavaScript's interface small and leaves the rules in Rust</p>\n<p>JavaScript gets a few operations chosen by the adapter. It cannot reach into the engine and start calculating a second version of a rule.</p>\n</div>\n</div>\n</section>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>It compiles with nothing to say. On your own machine, <code>cargo build</code> still reads the attribute and checks that everything it marks <em>could</em> cross to JavaScript, which is about to matter. But it exports nothing. The result is a normal library, which is why <code>cargo test</code> keeps working, and we will lean on that before we are done.</p>\n<hr />\n<h2 id=\"only-some-things-can-cross\">Only some things can cross<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#only-some-things-can-cross\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>buy_click_upgrade()</code> is next. The engine's version returns a <code>PurchaseOutcome</code>. Try handing that straight across. Add this inside the <code>impl WebGame</code> block:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">    #[wasm_bindgen(js_name = buyClickUpgrade)]\n    pub fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.inner.buy_click_upgrade()\n    }\n</code></pre></figure>\n<p>And add <code>use crate::player::PurchaseOutcome;</code> under the first <code>use</code> line.</p>\n<p>The <code>js_name</code> attribute works on methods too. Rust calls it <code>buy_click_upgrade</code>, because that is how Rust names things. JavaScript calls it <code>buyClickUpgrade</code>, because that is how JavaScript names things. Each side gets its own convention and neither has to flinch.</p>\n<p>Build, and Rust refuses, because this type is not allowed across:</p>\n<pre><code class=\"language-text\">error[E0277]: the trait bound `PurchaseOutcome: IntoWasmAbi` is not satisfied\n  --&gt; src/web.rs:10:1\n   |\n10 | #[wasm_bindgen(js_class = Game)]\n   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound\n   |\nhelp: the trait `IntoWasmAbi` is not implemented for `PurchaseOutcome`\n</code></pre>\n<p>(There are several of these, and one names a cousin trait, <code>WasmDescribe</code>. They all say the same thing: this type cannot cross.)</p>\n<p><code>IntoWasmAbi</code> is the trait for <em>can be handed to JavaScript</em>. Numbers have it. <code>bool</code> has it. <code>String</code> and <code>&amp;str</code> have it, thanks to the glue. Types marked with <code>#[wasm_bindgen]</code> have it.</p>\n<p><code>PurchaseOutcome</code> does not, and we are not going to mark it, because it belongs to the engine.</p>\n<p>So what crosses instead?</p>\n<p><code>false</code> is still not an answer. We settled that in the seventh article, and a border does not change it. What crosses is the <em>name</em> of the answer, as text:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">    #[wasm_bindgen(js_name = buyClickUpgrade)]\n    pub fn buy_click_upgrade(&amp;mut self) -&gt; String {\n        match self.inner.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; String::from(\"purchased\"),\n            PurchaseOutcome::NotEnoughGold =&gt; String::from(\"notEnoughGold\"),\n        }\n    }\n</code></pre></figure>\n<p>Replace the version you just wrote with this one.</p>\n<p>The <code>match</code> is doing the translating. Rust's enum stays an enum. JavaScript gets a string it can compare against. If a third outcome ever appears, the <code>match</code> refuses to compile until it has a name too, which is the same protection we had before.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What crosses into JavaScript when an upgrade cannot be bought?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The whole <code>PurchaseOutcome</code> enum definition</li>\n<li><code>false</code></li>\n<li>The string <code>\"notEnoughGold\"</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. The string <code>\"notEnoughGold\"</code></p>\n<p>The adapter translates the Rust enum variant into text. The engine keeps the enum, including Rust's exhaustive <code>match</code> check.</p>\n</div>\n</div>\n</section>\n<p>Build, and it is quiet again.</p>\n<hr />\n<h2 id=\"a-snapshot-for-the-screen\">A snapshot for the screen<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#a-snapshot-for-the-screen\" aria-label=\"Link to this section\">#</a></h2>\n<p>If JavaScript ever computes click damage, there are two programs that know how, and one day they will disagree.</p>\n<p>That is the rule this whole section protects.</p>\n<p>The browser is going to draw a screen, and a screen needs numbers: the stage, the gold, how much a click does, what the next upgrade costs. Some of those are fields. Some are rules. We could hand JavaScript the fields and let it work out the rest, and that is exactly the thing it must never do.</p>\n<p>So the adapter answers all of it at once, as one piece of JSON, and Rust does every bit of arithmetic before the text leaves.</p>\n<p>Add this at the bottom of <code>web.rs</code>, after the <code>impl WebGame</code> block:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">#[derive(Serialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct Snapshot {\n    stage: u32,\n    gold: u32,\n    xp: u32,\n    xp_needed: u32,\n    level: u32,\n    click_damage: u32,\n    click_upgrade_level: u32,\n    click_upgrade_cost: u32,\n    enemy_name: String,\n    enemy_health: u32,\n    enemy_max_health: u32,\n}\n\nimpl Snapshot {\n    fn of(game: &amp;Game) -&gt; Snapshot {\n        Snapshot {\n            stage: game.stage,\n            gold: game.player.gold,\n            xp: game.player.xp,\n            xp_needed: game.player.xp_needed(),\n            level: game.player.level,\n            click_damage: game.player.click_damage(),\n            click_upgrade_level: game.player.click_upgrade_level,\n            click_upgrade_cost: game.player.click_upgrade_cost(),\n            enemy_name: game.enemy.name.clone(),\n            enemy_health: game.enemy.health,\n            enemy_max_health: enemy_health_for_stage(game.stage),\n        }\n    }\n}\n</code></pre></figure>\n<p>And at the top of the file, two more <code>use</code> lines, under the one for <code>PurchaseOutcome</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">use crate::rules::enemy_health_for_stage;\nuse serde::Serialize;\n</code></pre></figure>\n<p>A <code>Snapshot</code> is a flat list of everything a screen might want, taken at one moment. It only needs <code>Serialize</code>, not <code>Deserialize</code>, because nobody ever reads a snapshot back in. It is a picture, not a save.</p>\n<p>Three lines in <code>of()</code> deserve a look.</p>\n<p><code>xp_needed</code>, <code>click_damage</code> and <code>click_upgrade_cost</code> are not copied. They are <em>asked for</em>. Each one is a rule, applied in Rust, and only the answer goes in the picture.</p>\n<p><code>enemy_max_health</code> is the same idea. The enemy does not store its full health; the rule <code>enemy_health_for_stage()</code> knows it, and the stage is right there.</p>\n<p><code>game.enemy.name.clone()</code> is new. A <code>String</code> owns its text, and two things cannot own the same text at once. The enemy owns that name. The snapshot wants its own, so <code>.clone()</code> makes a second copy. (The <code>u32</code> fields do not need this. A number is small enough that Rust copies it without asking.)</p>\n<p>And the attribute above the struct:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">#[serde(rename_all = \"camelCase\")]\n</code></pre></figure>\n<p>This is the article where JavaScript finally reads our JSON, so this is the article where the names change shape. Rust writes <code>click_damage</code>. JavaScript expects <code>clickDamage</code>. <code>rename_all</code> tells <code>serde</code> to convert every field name on the way out, and the Rust code stays the way Rust likes it.</p>\n<p>The save is not getting this. JavaScript stores a save and hands it back; it never looks inside. Changing the save's field names would be a new save version for the benefit of nobody.</p>\n<p>Two small things remain before it builds.</p>\n<p><code>click_upgrade_cost()</code> does not exist on <code>Player</code> yet. The cost has only ever been needed <em>inside</em> a purchase. Now a screen wants to show it. In <code>src/player.rs</code>, add a method under <code>click_damage()</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/player.rs</figcaption><pre><code class=\"language-rust\">    pub fn click_upgrade_cost(&amp;self) -&gt; u32 {\n        click_upgrade_cost(self.click_upgrade_level)\n    }\n</code></pre></figure>\n<p>And since the player can answer that now, <code>buy_click_upgrade()</code> should ask instead of reaching for the rule. Change its first line from:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/player.rs</figcaption><pre><code class=\"language-rust\">        let cost = click_upgrade_cost(self.click_upgrade_level);\n</code></pre></figure>\n<p>to:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/player.rs</figcaption><pre><code class=\"language-rust\">        let cost = self.click_upgrade_cost();\n</code></pre></figure>\n<p>Then the method that hands the picture over. Back in <code>web.rs</code>, inside <code>impl WebGame</code>, under <code>buy_click_upgrade()</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">    #[wasm_bindgen(js_name = stateJson)]\n    pub fn state_json(&amp;self) -&gt; String {\n        let snapshot = Snapshot::of(&amp;self.inner);\n\n        serde_json::to_string(&amp;snapshot).expect(\"a Snapshot can always be written as JSON\")\n    }\n</code></pre></figure>\n<p>That <code>expect()</code> is the same promise <code>save_json()</code> makes, for the same reason: a struct of numbers and one string cannot fail to become JSON.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does the snapshot contain <code>clickDamage</code> instead of making JavaScript calculate it?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>JavaScript cannot add two numbers</li>\n<li>Rust should remain the only place that knows the damage rule</li>\n<li>JSON requires every number to be calculated twice</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Rust should remain the only place that knows the damage rule</p>\n<p>The screen receives the answer it needs. If JavaScript repeated the formula, the two versions could disagree after a later rule change.</p>\n</div>\n</div>\n</section>\n<p>Build. Quiet.</p>\n<hr />\n<h2 id=\"saves-cross-too\">Saves cross too<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#saves-cross-too\" aria-label=\"Link to this section\">#</a></h2>\n<p>Saving is already text, so it crosses as it is:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">    #[wasm_bindgen(js_name = saveJson)]\n    pub fn save_json(&amp;self) -&gt; String {\n        self.inner.save_json()\n    }\n</code></pre></figure>\n<p>Loading is the interesting one, because loading can fail, and now the failure has to cross a border.</p>\n<p>Try the obvious thing first:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">    #[wasm_bindgen(js_name = fromJson)]\n    pub fn from_json(json: &amp;str) -&gt; Result&lt;WebGame, LoadError&gt; {\n        match Game::from_saved_json(json) {\n            Ok(inner) =&gt; Ok(WebGame { inner }),\n            Err(error) =&gt; Err(error),\n        }\n    }\n</code></pre></figure>\n<p>Add <code>use crate::save::LoadError;</code> to the <code>use</code> lines, and build. Rust refuses again, and this time the complaint is about the error half:</p>\n<pre><code class=\"language-text\">error[E0277]: the trait bound `Result&lt;WebGame, LoadError&gt;: ReturnWasmAbi` is not satisfied\n   --&gt; src/web.rs:13:1\n    |\n 13 | #[wasm_bindgen(js_class = Game)]\n    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ReturnWasmAbi` is not implemented for `Result&lt;WebGame, LoadError&gt;`\n    |\nhelp: the trait `ReturnWasmAbi` is implemented for `Result&lt;T, E&gt;`\n    |\n779 | / impl&lt;T, E&gt; ReturnWasmAbi for Result&lt;T, E&gt;\n780 | | where\n781 | |     T: IntoWasmAbi,\n782 | |     E: Into&lt;JsValue&gt;,\n</code></pre>\n<p>Same story as the purchase, with one new piece of information at the bottom.</p>\n<p>A <code>Result</code> <em>can</em> cross, as long as both halves can. <code>WebGame</code> is fine. <code>LoadError</code> is not, and the <code>help</code> says what the error half needs to be: something that can become a <code>JsValue</code>, which is the crate's word for <em>any JavaScript value</em>.</p>\n<p>A <code>String</code> can. So the adapter translates the error the same way it translated the purchase: into words.</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">    #[wasm_bindgen(js_name = fromJson)]\n    pub fn from_json(json: &amp;str) -&gt; Result&lt;WebGame, String&gt; {\n        match Game::from_saved_json(json) {\n            Ok(inner) =&gt; Ok(WebGame { inner }),\n            Err(LoadError::Unreadable) =&gt; Err(String::from(\"That save could not be read.\")),\n            Err(LoadError::UnsupportedVersion(version)) =&gt; Err(format!(\n                \"That save is version {version}, and this game cannot read it.\"\n            )),\n        }\n    }\n</code></pre></figure>\n<p><code>main()</code> prints a failed load with <code>{error:?}</code>, which shows the variant's name: <code>Could not load the save: Unreadable</code>. That is fine in a terminal only we read. A player deserves a sentence, so the adapter writes one for each way a load can fail.</p>\n<p><code>format!()</code> is <code>println!()</code>'s quieter sibling. Same curly braces, but instead of printing the result, it hands it back as a <code>String</code>.</p>\n<p>What does JavaScript see when the <code>Result</code> is an <code>Err</code>? The glue <em>throws</em> it. On the other side, <code>Game.fromJson(text)</code> either returns a game or throws the message, and a <code>try</code>/<code>catch</code> catches it. We will use that next time, when there is a save to load.</p>\n<p>Build. Quiet. Here is <code>web.rs</code> so far, for checking against. The <code>Snapshot</code> code at the bottom is unchanged:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">use crate::game::Game;\nuse crate::player::PurchaseOutcome;\nuse crate::rules::enemy_health_for_stage;\nuse crate::save::LoadError;\nuse serde::Serialize;\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen(js_name = Game)]\npub struct WebGame {\n    inner: Game,\n}\n\n#[wasm_bindgen(js_class = Game)]\nimpl WebGame {\n    #[wasm_bindgen(constructor)]\n    pub fn new() -&gt; WebGame {\n        WebGame { inner: Game::new() }\n    }\n\n    pub fn click(&amp;mut self) {\n        self.inner.click();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        self.inner.tick(seconds);\n    }\n\n    #[wasm_bindgen(js_name = buyClickUpgrade)]\n    pub fn buy_click_upgrade(&amp;mut self) -&gt; String {\n        match self.inner.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; String::from(\"purchased\"),\n            PurchaseOutcome::NotEnoughGold =&gt; String::from(\"notEnoughGold\"),\n        }\n    }\n\n    #[wasm_bindgen(js_name = stateJson)]\n    pub fn state_json(&amp;self) -&gt; String {\n        let snapshot = Snapshot::of(&amp;self.inner);\n\n        serde_json::to_string(&amp;snapshot).expect(\"a Snapshot can always be written as JSON\")\n    }\n\n    #[wasm_bindgen(js_name = saveJson)]\n    pub fn save_json(&amp;self) -&gt; String {\n        self.inner.save_json()\n    }\n\n    #[wasm_bindgen(js_name = fromJson)]\n    pub fn from_json(json: &amp;str) -&gt; Result&lt;WebGame, String&gt; {\n        match Game::from_saved_json(json) {\n            Ok(inner) =&gt; Ok(WebGame { inner }),\n            Err(LoadError::Unreadable) =&gt; Err(String::from(\"That save could not be read.\")),\n            Err(LoadError::UnsupportedVersion(version)) =&gt; Err(format!(\n                \"That save is version {version}, and this game cannot read it.\"\n            )),\n        }\n    }\n}\n</code></pre></figure>\n<p>Seven things JavaScript can do. That is the whole border.</p>\n<hr />\n<h2 id=\"test-it-where-you-are\">Test it where you are<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#test-it-where-you-are\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have not compiled for WebAssembly yet, and we can already test the adapter, because on this machine it is ordinary Rust.</p>\n<p>Add a <code>tests</code> module at the bottom of <code>web.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn a_new_game_reports_its_starting_state() {\n        let game = WebGame::new();\n\n        assert_eq!(\n            game.state_json(),\n            r#\"{\"stage\":1,\"gold\":0,\"xp\":0,\"xpNeeded\":20,\"level\":1,\"clickDamage\":5,\"clickUpgradeLevel\":0,\"clickUpgradeCost\":10,\"enemyName\":\"Training Slime\",\"enemyHealth\":20,\"enemyMaxHealth\":20}\"#\n        );\n    }\n\n    #[test]\n    fn a_purchase_answers_in_words() {\n        let mut game = WebGame::new();\n\n        assert_eq!(game.buy_click_upgrade(), \"notEnoughGold\");\n\n        game.tick(100);\n\n        assert_eq!(game.buy_click_upgrade(), \"purchased\");\n    }\n\n    #[test]\n    fn a_save_crosses_the_border_and_back() {\n        let mut game = WebGame::new();\n        game.tick(600);\n\n        let save = game.save_json();\n        let restored = WebGame::from_json(&amp;save).expect(\"the save should load\");\n\n        assert_eq!(restored.state_json(), game.state_json());\n    }\n\n    #[test]\n    fn nonsense_is_refused_at_the_border() {\n        assert_eq!(\n            WebGame::from_json(\"banana\").err(),\n            Some(String::from(\"That save could not be read.\"))\n        );\n    }\n}\n</code></pre></figure>\n<p>The first test is the one to read slowly. That is the exact text a browser will receive for a new game: every name in camelCase, every rule already applied. Click damage is 5 because level 1 hits for 5. The upgrade costs 10. The slime has 20 of 20.</p>\n<p>The last test uses <code>.err()</code>, which turns a <code>Result</code> into an <code>Option</code>: <code>Some(error)</code> if it failed, <code>None</code> if it did not. We cannot <code>assert_eq!</code> on the whole <code>Result</code>, because <code>WebGame</code> has no <code>PartialEq</code> and does not need one. Comparing just the error half is enough.</p>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Thirty-two. Thirty in the library, and the two outside tests.</p>\n<p>If you see twenty-eight, the <code>mod web;</code> line is missing from <code>lib.rs</code>. Without it, the file is not part of the library at all, and nothing complains until much later, in the browser. Add the line and run the tests again.</p>\n<hr />\n<h2 id=\"build-for-the-browser\">Build for the browser<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#build-for-the-browser\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now the other machine.</p>\n<pre><code class=\"language-bash\">wasm-pack build --target web --out-dir web/pkg\n</code></pre>\n<p><code>--target web</code> asks for glue that a plain web page can import, with no bundler and no Node. <code>--out-dir web/pkg</code> says where to put it.</p>\n<p>The first run is slow. <code>wasm-pack</code> fetches the <code>wasm-bindgen</code> command-line program to match the crate version in <code>Cargo.lock</code>, compiles the library for <code>wasm32-unknown-unknown</code> in release mode, runs the glue generator, and shrinks the result. After that it is a few seconds.</p>\n<p>When it finishes, the last lines look like this:</p>\n<pre><code class=\"language-text\">[INFO]: Optimizing wasm binaries with `wasm-opt`...\n[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended\n[INFO]: :-) Done in 24.46s\n[INFO]: :-) Your wasm pkg is ready to publish at /home/you/rustbound-idle/web/pkg.\n</code></pre>\n<p>Your time and path will differ, and the note about optional fields is safe to ignore. Look in the folder:</p>\n<pre><code class=\"language-text\">web/pkg/\n├── .gitignore\n├── package.json\n├── rustbound_idle.d.ts\n├── rustbound_idle.js\n├── rustbound_idle_bg.wasm\n└── rustbound_idle_bg.wasm.d.ts\n</code></pre>\n<p><code>rustbound_idle_bg.wasm</code> is the compiled game, about 110 KB. (We will look at that number again when it is time to ship.) <code>rustbound_idle.js</code> is the glue: the JavaScript that loads the <code>.wasm</code> file and wraps every marked method so that strings go in and out as strings. <code>package.json</code> describes the folder to JavaScript tooling we are not using.</p>\n<p>The two <code>.d.ts</code> files are the ones to open. They are written for TypeScript, a version of JavaScript with types, but you do not need to know TypeScript to read this; it is a list of everything JavaScript can call, and it is the clearest summary of what we just built. The <code>Game</code> class is near the top of <code>rustbound_idle.d.ts</code>; everything below it is glue you can ignore:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">web/pkg/rustbound_idle.d.ts</figcaption><pre><code class=\"language-ts\">export class Game {\n    free(): void;\n    [Symbol.dispose](): void;\n    buyClickUpgrade(): string;\n    click(): void;\n    static fromJson(json: string): Game;\n    constructor();\n    saveJson(): string;\n    stateJson(): string;\n    tick(seconds: number): void;\n}\n</code></pre></figure>\n<p>Every <code>js_name</code> we chose is in there. <code>fromJson</code> is <code>static</code>, because it makes a <code>Game</code> rather than being called on one, which is the same reason <code>from_saved_json()</code> was an associated function.</p>\n<p><code>free()</code> and <code>[Symbol.dispose]</code> are the two we did not write. A <code>Game</code> on the Rust side lives in WebAssembly's memory, and JavaScript's garbage collector cannot see in there. <code>free()</code> is how JavaScript gives a game back when it is done with it, and the other is a newer spelling of the same thing. We will not need either today.</p>\n<p>The <code>.gitignore</code> inside <code>pkg/</code> contains a single <code>*</code>. The whole folder is generated, like <code>target/</code>, and it stays out of version control.</p>\n<p>If instead you see this:</p>\n<pre><code class=\"language-text\">Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n</code></pre>\n<p>then the <code>[lib]</code> section did not make it into <code>Cargo.toml</code>. Add it and run the build again.</p>\n<p>And if the build succeeds but the <code>.d.ts</code> file has no <code>Game</code> class in it, <code>mod web;</code> is missing from <code>lib.rs</code>. The page will later complain that the module \"does not provide an export named 'Game'\", which is the same problem seen from the other side.</p>\n<hr />\n<h2 id=\"a-page-that-runs-the-engine\">A page that runs the engine<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#a-page-that-runs-the-engine\" aria-label=\"Link to this section\">#</a></h2>\n<p>Create <code>web/index.html</code>, next to the <code>pkg</code> folder:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">web/index.html</figcaption><pre><code class=\"language-html\">&lt;!doctype html&gt;\n&lt;html lang=\"en\"&gt;\n  &lt;head&gt;\n    &lt;meta charset=\"UTF-8\" /&gt;\n    &lt;title&gt;Rustbound Idle&lt;/title&gt;\n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;h1&gt;Rustbound Idle&lt;/h1&gt;\n    &lt;pre id=\"output\"&gt;Loading the engine...&lt;/pre&gt;\n\n    &lt;script type=\"module\"&gt;\n      import init, { Game } from \"./pkg/rustbound_idle.js\";\n\n      await init();\n\n      const game = new Game();\n      const before = JSON.parse(game.stateJson());\n\n      game.tick(3600);\n\n      const after = JSON.parse(game.stateJson());\n\n      document.querySelector(\"#output\").textContent =\n        `One hour away.\\nStage ${before.stage} -&gt; ${after.stage}  ` +\n        `Level ${before.level} -&gt; ${after.level}  ` +\n        `Gold ${before.gold} -&gt; ${after.gold}`;\n    &lt;/script&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</code></pre></figure>\n<p>The script is the whole point, so read it line by line.</p>\n<p><code>import init, { Game } from \"./pkg/rustbound_idle.js\";</code> brings in two things from the glue. <code>Game</code> is our class. <code>init</code> is a function that fetches <code>rustbound_idle_bg.wasm</code> and loads it into the browser.</p>\n<p><code>await init();</code> does that loading. It is the one step that takes time, which is why the page says \"Loading the engine...\" until it is done. (If you have been told <code>await</code> only works inside an <code>async</code> function: at the top of a <code>&lt;script type=\"module\"&gt;</code> it is allowed. That is one of the things <code>type=\"module\"</code> buys us.)</p>\n<p><code>new Game()</code> runs our constructor. <code>game.stateJson()</code> runs our snapshot. <code>game.tick(3600)</code> runs the same loop that has run in every test since the eighth article.</p>\n<p><code>JSON.parse()</code> turns our text into a JavaScript object, so <code>before.stage</code> and <code>before.gold</code> just work. The names are camelCase, and that is <code>rename_all</code> paying off.</p>\n<p>The sandbox below exercises only that browser-side handoff. It does not pretend to compile or load the generated <code>.wasm</code> package. Run it once, then change the three values in <code>after</code> and run it again. The sentence should follow the snapshot without learning a single game rule.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Live example · render the Rust snapshot</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h2&gt;Rustbound Idle&lt;/h2&gt;\n&lt;pre id=&quot;output&quot;&gt;Waiting for a snapshot...&lt;/pre&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">const snapshotText = &#39;{&quot;stage&quot;:1,&quot;gold&quot;:0,&quot;xp&quot;:0,&quot;xpNeeded&quot;:20,&quot;level&quot;:1,&quot;clickDamage&quot;:5,&quot;clickUpgradeLevel&quot;:0,&quot;clickUpgradeCost&quot;:10,&quot;enemyName&quot;:&quot;Training Slime&quot;,&quot;enemyHealth&quot;:20,&quot;enemyMaxHealth&quot;:20}&#39;;\n\nconst before = JSON.parse(snapshotText);\nconst after = {\n  ...before,\n  stage: 128,\n  level: 47,\n  gold: 16637,\n};\n\ndocument.querySelector(&quot;#output&quot;).textContent =\n  `One hour away.\\nStage ${before.stage} -&gt; ${after.stage}  ` +\n  `Level ${before.level} -&gt; ${after.level}  ` +\n  `Gold ${before.gold} -&gt; ${after.gold}`;\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/\">the original article</a>.</p></figure>\n<p>Then the last lines print the same sentence <code>main()</code> prints. Everything else on the page is scaffolding.</p>\n<p>Now serve it. A browser will not load a module or a <code>.wasm</code> file from a plain file on disk; both have to come from a web server, even a trivial one. Python ships with a trivial one:</p>\n<pre><code class=\"language-bash\">python3 -m http.server 8080 --directory web\n</code></pre>\n<p>(On Windows the command is usually <code>python</code> rather than <code>python3</code>.)</p>\n<p>Open <code>http://localhost:8080/</code> in a browser.</p>\n<pre><code class=\"language-text\">Rustbound Idle\n\nOne hour away.\nStage 1 -&gt; 128  Level 1 -&gt; 47  Gold 0 -&gt; 16637\n</code></pre>\n<p>Stage 128. Level 47.</p>\n<p>Those are the numbers in <code>tests/idle_hour.rs</code>, and they have been since the ninth article. The test runs on your machine, in Rust. The page runs in a browser, in WebAssembly, called from JavaScript. Same library. Same answer.</p>\n<p>If you open <code>index.html</code> directly instead, by double-clicking it, the page stays on \"Loading the engine...\" and the browser console says something like:</p>\n<pre><code class=\"language-text\">Access to script at 'file:///.../web/pkg/rustbound_idle.js' from origin 'null'\nhas been blocked by CORS policy: Cross origin requests are only supported for\nprotocol schemes: chrome, chrome-untrusted, data, http, https.\n</code></pre>\n<p>That is the browser refusing to load modules from <code>file://</code>. It is not a bug in our code. Use the server.</p>\n<p>One habit to pick up now: after you change the Rust and rebuild, browsers like to keep serving the old <code>.wasm</code> file. If a number on the page does not change when you expected it to, do a hard refresh (Ctrl+Shift+R, or Cmd+Shift+R on a Mac) before you go looking for a bug.</p>\n<p>One more thing to notice: <code>cargo run</code> still works. Try it. The terminal version prints its three stages and its hour, exactly as before. We did not replace a program. We gave the same library a second door.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along, here is everything that changed. <code>src/rules.rs</code>, <code>src/enemy.rs</code>, <code>src/game.rs</code>, <code>src/save.rs</code>, <code>src/main.rs</code>, and the two files in <code>tests/</code> are exactly as they were in the starting point.</p>\n<p>The project now looks like this:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n├── Cargo.lock\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   ├── game.rs\n│   ├── save.rs\n│   └── web.rs\n├── tests/\n│   ├── idle_hour.rs\n│   └── save_and_restore.rs\n└── web/\n    ├── index.html\n    └── pkg/            (generated by wasm-pack)\n</code></pre>\n<p><code>Cargo.toml</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">Cargo.toml</figcaption><pre><code class=\"language-toml\">[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nwasm-bindgen = \"0.2\"\n</code></pre></figure>\n<p><code>src/lib.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/lib.rs</figcaption><pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\nmod web;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use save::LoadError;\n</code></pre></figure>\n<p>Only the <code>impl</code> block in <code>src/player.rs</code> changes; the struct, the enum, and the tests are as they were.</p>\n<details>\n<summary><code>src/player.rs</code> · changed 56-line <code>impl</code></summary>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/player.rs</figcaption><pre><code class=\"language-rust\">impl Player {\n    pub(crate) fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    pub(crate) fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    pub(crate) fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= self.xp_needed() {\n            self.xp -= self.xp_needed();\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    pub fn xp_needed(&amp;self) -&gt; u32 {\n        xp_to_next_level(self.level)\n    }\n\n    pub fn click_damage(&amp;self) -&gt; u32 {\n        click_damage(self.base_damage(), self.bonus_damage())\n    }\n\n    pub fn click_upgrade_cost(&amp;self) -&gt; u32 {\n        click_upgrade_cost(self.click_upgrade_level)\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    pub(crate) fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = self.click_upgrade_cost();\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n</code></pre></figure>\n</details>\n<details>\n<summary><code>src/web.rs</code> · 137 lines</summary>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/web.rs</figcaption><pre><code class=\"language-rust\">use crate::game::Game;\nuse crate::player::PurchaseOutcome;\nuse crate::rules::enemy_health_for_stage;\nuse crate::save::LoadError;\nuse serde::Serialize;\nuse wasm_bindgen::prelude::*;\n\n#[wasm_bindgen(js_name = Game)]\npub struct WebGame {\n    inner: Game,\n}\n\n#[wasm_bindgen(js_class = Game)]\nimpl WebGame {\n    #[wasm_bindgen(constructor)]\n    pub fn new() -&gt; WebGame {\n        WebGame { inner: Game::new() }\n    }\n\n    pub fn click(&amp;mut self) {\n        self.inner.click();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        self.inner.tick(seconds);\n    }\n\n    #[wasm_bindgen(js_name = buyClickUpgrade)]\n    pub fn buy_click_upgrade(&amp;mut self) -&gt; String {\n        match self.inner.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; String::from(\"purchased\"),\n            PurchaseOutcome::NotEnoughGold =&gt; String::from(\"notEnoughGold\"),\n        }\n    }\n\n    #[wasm_bindgen(js_name = stateJson)]\n    pub fn state_json(&amp;self) -&gt; String {\n        let snapshot = Snapshot::of(&amp;self.inner);\n\n        serde_json::to_string(&amp;snapshot).expect(\"a Snapshot can always be written as JSON\")\n    }\n\n    #[wasm_bindgen(js_name = saveJson)]\n    pub fn save_json(&amp;self) -&gt; String {\n        self.inner.save_json()\n    }\n\n    #[wasm_bindgen(js_name = fromJson)]\n    pub fn from_json(json: &amp;str) -&gt; Result&lt;WebGame, String&gt; {\n        match Game::from_saved_json(json) {\n            Ok(inner) =&gt; Ok(WebGame { inner }),\n            Err(LoadError::Unreadable) =&gt; Err(String::from(\"That save could not be read.\")),\n            Err(LoadError::UnsupportedVersion(version)) =&gt; Err(format!(\n                \"That save is version {version}, and this game cannot read it.\"\n            )),\n        }\n    }\n}\n\n#[derive(Serialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct Snapshot {\n    stage: u32,\n    gold: u32,\n    xp: u32,\n    xp_needed: u32,\n    level: u32,\n    click_damage: u32,\n    click_upgrade_level: u32,\n    click_upgrade_cost: u32,\n    enemy_name: String,\n    enemy_health: u32,\n    enemy_max_health: u32,\n}\n\nimpl Snapshot {\n    fn of(game: &amp;Game) -&gt; Snapshot {\n        Snapshot {\n            stage: game.stage,\n            gold: game.player.gold,\n            xp: game.player.xp,\n            xp_needed: game.player.xp_needed(),\n            level: game.player.level,\n            click_damage: game.player.click_damage(),\n            click_upgrade_level: game.player.click_upgrade_level,\n            click_upgrade_cost: game.player.click_upgrade_cost(),\n            enemy_name: game.enemy.name.clone(),\n            enemy_health: game.enemy.health,\n            enemy_max_health: enemy_health_for_stage(game.stage),\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn a_new_game_reports_its_starting_state() {\n        let game = WebGame::new();\n\n        assert_eq!(\n            game.state_json(),\n            r#\"{\"stage\":1,\"gold\":0,\"xp\":0,\"xpNeeded\":20,\"level\":1,\"clickDamage\":5,\"clickUpgradeLevel\":0,\"clickUpgradeCost\":10,\"enemyName\":\"Training Slime\",\"enemyHealth\":20,\"enemyMaxHealth\":20}\"#\n        );\n    }\n\n    #[test]\n    fn a_purchase_answers_in_words() {\n        let mut game = WebGame::new();\n\n        assert_eq!(game.buy_click_upgrade(), \"notEnoughGold\");\n\n        game.tick(100);\n\n        assert_eq!(game.buy_click_upgrade(), \"purchased\");\n    }\n\n    #[test]\n    fn a_save_crosses_the_border_and_back() {\n        let mut game = WebGame::new();\n        game.tick(600);\n\n        let save = game.save_json();\n        let restored = WebGame::from_json(&amp;save).expect(\"the save should load\");\n\n        assert_eq!(restored.state_json(), game.state_json());\n    }\n\n    #[test]\n    fn nonsense_is_refused_at_the_border() {\n        assert_eq!(\n            WebGame::from_json(\"banana\").err(),\n            Some(String::from(\"That save could not be read.\"))\n        );\n    }\n}\n</code></pre></figure>\n</details>\n<details>\n<summary><code>web/index.html</code> · 29 lines</summary>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">web/index.html</figcaption><pre><code class=\"language-html\">&lt;!doctype html&gt;\n&lt;html lang=\"en\"&gt;\n  &lt;head&gt;\n    &lt;meta charset=\"UTF-8\" /&gt;\n    &lt;title&gt;Rustbound Idle&lt;/title&gt;\n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;h1&gt;Rustbound Idle&lt;/h1&gt;\n    &lt;pre id=\"output\"&gt;Loading the engine...&lt;/pre&gt;\n\n    &lt;script type=\"module\"&gt;\n      import init, { Game } from \"./pkg/rustbound_idle.js\";\n\n      await init();\n\n      const game = new Game();\n      const before = JSON.parse(game.stateJson());\n\n      game.tick(3600);\n\n      const after = JSON.parse(game.stateJson());\n\n      document.querySelector(\"#output\").textContent =\n        `One hour away.\\nStage ${before.stage} -&gt; ${after.stage}  ` +\n        `Level ${before.level} -&gt; ${after.level}  ` +\n        `Gold ${before.gold} -&gt; ${after.gold}`;\n    &lt;/script&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</code></pre></figure>\n</details>\n<p>You do not need to add <code>web/pkg</code> to your own <code>.gitignore</code>. The one <code>wasm-pack</code> wrote inside it already keeps the folder out.</p>\n<p>Run all three:</p>\n<pre><code class=\"language-bash\">cargo test\nwasm-pack build --target web --out-dir web/pkg\npython3 -m http.server 8080 --directory web\n</code></pre>\n<p>Thirty-two tests pass, the package builds, and the page at <code>http://localhost:8080/</code> shows the hour away. Clippy should be quiet.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>compile targets, and <code>wasm32-unknown-unknown</code></li>\n<li>what WebAssembly can and cannot do on its own</li>\n<li><code>wasm-bindgen</code> and <code>wasm-pack</code></li>\n<li><code>cargo install</code></li>\n<li><code>[lib] crate-type</code>, and the difference between <code>rlib</code> and <code>cdylib</code></li>\n<li><code>#[wasm_bindgen]</code>, <code>js_name</code>, <code>js_class</code>, and <code>constructor</code></li>\n<li>a prelude, and <code>use ...::*</code></li>\n<li>an adapter that holds the engine instead of exposing it</li>\n<li>which types can cross a language border, and translating the ones that cannot</li>\n<li>a snapshot that applies every rule before the text leaves Rust</li>\n<li><code>#[serde(rename_all = \"camelCase\")]</code></li>\n<li><code>.clone()</code> for a <code>String</code></li>\n<li><code>format!()</code></li>\n<li>a <code>Result</code> crossing into JavaScript as a return or a throw</li>\n<li><code>.err()</code> on a <code>Result</code></li>\n<li>testing a WebAssembly adapter as plain Rust</li>\n<li>ES modules, <code>import</code>, <code>await init()</code>, and why <code>file://</code> will not do</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>Two ideas are worth keeping.</p>\n<p>The first is that the border is a translation, not a window. Nothing on the far side can see <code>Game</code>. It sees seven operations, each of which hands numbers or text across, and every rule has already been applied by the time the text leaves. The <code>match</code> in <code>buy_click_upgrade()</code> and the <code>match</code> in <code>from_json()</code> are the only places the two worlds touch, and both of them turn a Rust enum into words.</p>\n<p>The second is that the hard part was already done. This article added one file to the library, one line to <code>lib.rs</code>, a four-line method to <code>player.rs</code>, and four lines of configuration. Everything else was two articles ago, when we decided <code>main()</code> should not know any formulas, and last time, when we decided a save should be text. A library that never printed, never asked the time, and never opened a file turned out to be a library that compiles for a machine with none of those things.</p>\n<p>That is what the wall was for.</p>\n<hr />\n<h2 id=\"something-to-click\">Something to click<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#something-to-click\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our engine runs in a browser now. Nobody can play it.</p>\n<p>There is no button. There is no screen. There is a <code>&lt;pre&gt;</code> tag and a number.</p>\n<p>Next time, we put a face on it: one custom HTML element, <code>&lt;idle-game&gt;</code>, that draws the snapshot, turns a click on a button into <code>game.click()</code>, keeps the clock, and saves to the browser's storage so that closing the tab is not game over either.</p>\n<p>The browser will own time and storage and pixels.</p>\n<p>It still will not get the rules.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "JavaScript Does Not Get the Rules.\nCanonical: https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/\nCompile Your Rust Game to WebAssembly\nFor ten articles, the only thing that has ever run our game is main(), in Rust, in a terminal.\nToday, a browser runs it.\nNot a rewrite. Not a port. The same library, with the same twenty-eight tests, compiled for a different machine. The browser gets a handful of operations and a save that is already text. It does not get the rules.\nBy the end, a web page shows this:\nOne hour away.\nStage 1 -> 128  Level 1 -> 47  Gold 0 -> 16637\nStage 128 and level 47 are the numbers tests/idle_hour.rs has been checking since the ninth article. Same engine. Same hour. The call came from JavaScript.\nAlong the way we meet WebAssembly, two tools that do the glue work, and one new rule about what is allowed to cross a border.\nOur starting point\nThis series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.\nIf you are only reading along, skip past this code to \"A second machine.\"\nIf you are building along, you need the library as it stood at the end of the last article: a project called rustbound-idle with seven source files and two outside tests. (If you kept your project, you already have all of this. The article_10 branch of the companion repository has it too.)\nrustbound-idle/\n├── Cargo.toml\n├── Cargo.lock\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   ├── game.rs\n│   └── save.rs\n└── tests/\n├── idle_hour.rs\n└── save_and_restore.rs\nCargo.toml:\nCargo.toml\n[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n<details>\n<summary><code>src/lib.rs</code> · 10 lines</summary>\nmod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use save::LoadError;\n</details>\n<details>\n<summary><code>src/main.rs</code> · 71 lines</summary>\nuse rustbound_idle::{Game, PurchaseOutcome};\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\ngame.player.xp_needed()\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\ngame.player.click_damage()\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\ngame.player.click_damage(),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet save = game.save_json();\nprintln!(\"Saved: {save}\");\ndrop(game);\nlet mut game = match Game::from_saved_json(&save) {\nOk(game) => game,\nErr(error) => {\nprintln!(\"Could not load the save: {error:?}\");\nreturn;\n}\n};\nprintln!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n</details>\n<details>\n<summary><code>src/rules.rs</code> · 105 lines</summary>\npub(crate) fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\npub(crate) fn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\npub(crate) fn is_defeated(health: u32) -> bool {\nhealth == 0\n}\npub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\npub(crate) fn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\npub(crate) fn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\npub(crate) fn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\npub(crate) fn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\npub(crate) fn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\npub(crate) fn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\npub(crate) fn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n}\n</details>\n<details>\n<summary><code>src/enemy.rs</code> · 19 lines</summary>\nuse crate::rules::{enemy_health_for_stage, gold_reward_for_stage, xp_reward_for_stage};\npub struct Enemy {\npub name: String,\npub health: u32,\npub(crate) gold_reward: u32,\npub(crate) xp_reward: u32,\n}\nimpl Enemy {\npub(crate) fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\n</details>\n<details>\n<summary><code>src/player.rs</code> · 114 lines</summary>\nuse crate::rules::{\nbase_damage_for_level, click_damage, click_upgrade_bonus, click_upgrade_cost, xp_to_next_level,\n};\npub struct Player {\npub gold: u32,\npub xp: u32,\npub level: u32,\npub click_upgrade_level: u32,\n}\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\npub(crate) fn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\npub(crate) fn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\npub(crate) fn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\n}\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(self.base_damage(), self.bonus_damage())\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\npub(crate) fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n}\n</details>\n<details>\n<summary><code>src/game.rs</code> · 158 lines</summary>\nuse crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{health_after_hit, idle_damage_for_level, is_defeated};\npub struct Game {\npub stage: u32,\npub player: Player,\npub enemy: Enemy,\n}\nimpl Game {\npub fn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\npub fn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\npub fn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\npub fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nimpl Default for Game {\nfn default() -> Game {\nGame::new()\n}\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nenemy.health = health_after_hit(enemy.health, player.click_damage());\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\n}\n</details>\n<details>\n<summary><code>src/save.rs</code> · 130 lines</summary>\nuse crate::enemy::Enemy;\nuse crate::game::Game;\nuse serde::{Deserialize, Serialize};\nconst SAVE_VERSION: u32 = 1;\n#[derive(Debug, PartialEq)]\npub enum LoadError {\nUnreadable,\nUnsupportedVersion(u32),\n}\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\n#[derive(Serialize, Deserialize)]\nstruct SaveData {\nsave_version: u32,\nstage: u32,\nplayer: PlayerSave,\nenemy_health: u32,\n}\nimpl Game {\npub fn save_json(&self) -> String {\nlet save = SaveData {\nsave_version: SAVE_VERSION,\nstage: self.stage,\nplayer: PlayerSave {\ngold: self.player.gold,\nxp: self.player.xp,\nlevel: self.player.level,\nclick_upgrade_level: self.player.click_upgrade_level,\n},\nenemy_health: self.enemy.health,\n};\nserde_json::to_string(&save).expect(\"a save made of numbers should always serialize\")\n}\npub fn from_saved_json(json: &str) -> Result<Game, LoadError> {\nlet save: SaveData = match serde_json::from_str(json) {\nOk(save) => save,\nErr(_) => return Err(LoadError::Unreadable),\n};\nif save.save_version != SAVE_VERSION {\nreturn Err(LoadError::UnsupportedVersion(save.save_version));\n}\nlet mut game = Game::new();\ngame.stage = save.stage;\ngame.player.gold = save.player.gold;\ngame.player.xp = save.player.xp;\ngame.player.level = save.player.level;\ngame.player.click_upgrade_level = save.player.click_upgrade_level;\ngame.enemy = Enemy::for_stage(save.stage);\ngame.enemy.health = save.enemy_health;\nOk(game)\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn a_new_game_saves_its_starting_numbers() {\nlet json = Game::new().save_json();\nassert_eq!(\njson,\nr#\"{\"save_version\":1,\"stage\":1,\"player\":{\"gold\":0,\"xp\":0,\"level\":1,\"click_upgrade_level\":0},\"enemy_health\":20}\"#\n);\n}\n#[test]\nfn a_loaded_game_rebuilds_the_enemy_from_the_rules() {\nlet mut game = Game::new();\ngame.tick(600);\nlet restored = Game::from_saved_json(&game.save_json()).expect(\"the save should load\");\nassert_eq!(restored.stage, game.stage);\nassert_eq!(restored.enemy.health, game.enemy.health);\nassert_eq!(restored.enemy.gold_reward, game.enemy.gold_reward);\nassert_eq!(restored.enemy.xp_reward, game.enemy.xp_reward);\n}\n#[test]\nfn nonsense_is_not_a_save() {\nassert!(matches!(\nGame::from_saved_json(\"definitely not a save\"),\nErr(LoadError::Unreadable)\n));\n}\n#[test]\nfn a_save_with_missing_pieces_is_not_a_save() {\nlet json = r#\"{\"save_version\":1,\"stage\":4}\"#;\nassert!(matches!(\nGame::from_saved_json(json),\nErr(LoadError::Unreadable)\n));\n}\n#[test]\nfn a_save_from_another_version_is_refused() {\nlet json = r#\"{\n\"save_version\": 99,\n\"stage\": 1,\n\"player\": { \"gold\": 0, \"xp\": 0, \"level\": 1, \"click_upgrade_level\": 0 },\n\"enemy_health\": 20\n}\"#;\nassert!(matches!(\nGame::from_saved_json(json),\nErr(LoadError::UnsupportedVersion(99))\n));\n}\n}\n</details>\n<details>\n<summary><code>tests/idle_hour.rs</code> · 11 lines</summary>\nuse rustbound_idle::Game;\n#[test]\nfn an_hour_away_moves_the_game_forward() {\nlet mut game = Game::new();\ngame.tick(3600);\nassert_eq!(game.stage, 128);\nassert_eq!(game.player.level, 47);\n}\n</details>\n<details>\n<summary><code>tests/save_and_restore.rs</code> · 18 lines</summary>\nuse rustbound_idle::Game;\n#[test]\nfn a_restored_game_plays_the_same_hour() {\nlet mut original = Game::new();\noriginal.tick(600);\nlet mut restored = Game::from_saved_json(&original.save_json()).expect(\"the save should load\");\noriginal.tick(3600);\nrestored.tick(3600);\nassert_eq!(restored.stage, original.stage);\nassert_eq!(restored.enemy.health, original.enemy.health);\nassert_eq!(restored.player.gold, original.player.gold);\nassert_eq!(restored.player.xp, original.player.xp);\nassert_eq!(restored.player.level, original.player.level);\n}\n</details>\nRun cargo test, and twenty-eight tests pass. Run cargo run, and the player clears three stages, saves, loads, and spends an hour away.\nA second machine\nEverything we have compiled so far has been for the computer in front of you. cargo build asks Rust to produce a program for this machine: this processor, this operating system.\nRust can produce programs for other machines too. A phone. A server with a different processor. Each one is called a target, and you pick it when you build.\nWebAssembly is one of those targets.\nIt is not a real machine. It is a description of one, agreed on by the people who make browsers, so that every browser can run the same compiled code. Compile Rust for it, and the result is a file a browser can load and run, the way it loads and runs JavaScript.\nThe target has a long name:\nwasm32-unknown-unknown\nThe three parts are the processor, the vendor, and the operating system. The processor is 32-bit WebAssembly. The other two are unknown, and they mean it.\nThere is no operating system. No files, no clock, no terminal, no println!. A program compiled for this target can do arithmetic and talk to whoever loaded it, and that is all.\nThat should sound like a problem.\nIt is not, and the reason is the wall we built two articles ago. Our library has never opened a file, never asked the time, never printed. It takes numbers and text, and it gives back numbers and text. main.rs does the printing, and main.rs is not coming with us.\nInstall the target:\nrustup target add wasm32-unknown-unknown\nThat downloads Rust's standard library, compiled for WebAssembly, so that String and Vec and saturating_add all exist on the other side. It takes a moment, and it only happens once.\nQuestion: Why does main.rs stay behind when we compile the library for the browser?\nWebAssembly can only compile files named lib.rs\nmain.rs prints to a terminal, and this browser target has no terminal\nJavaScript automatically replaces every Rust main() function\nAnswer: 2. main.rs prints to a terminal, and this browser target has no terminal\nExplanation: The engine only calculates with numbers and text. The terminal-specific printing stays in main.rs on the machine that has a terminal.\nTwo tools\nWebAssembly has one more limitation that matters to us.\nAt the raw WebAssembly boundary we are using today, values cross as WebAssembly's small built-in types. A Rust String or struct cannot cross as a JavaScript string or object by itself.\nOur front door takes a &str and returns a String. Those are not numbers.\nSomeone has to write the code that turns a Rust String into a JavaScript string, one byte at a time, on both sides of the line. That code is tedious, and it is the same every time, so a tool writes it. Two tools, in fact:\nwasm-bindgen is a Rust crate and a command-line program. The crate gives us an attribute to mark what JavaScript should be able to call. The program reads a compiled .wasm file and writes the JavaScript glue for everything marked.\nwasm-pack runs the whole build: cargo build for the WebAssembly target, then wasm-bindgen, then a shrinking pass, and puts the results in one folder, ready for a web page.\nInstall wasm-pack:\ncargo install wasm-pack\ncargo install builds a program from crates.io and puts it on your path. This one takes a couple of minutes.\nThe other tool comes in through Cargo.toml, the way serde did. Open it and make two changes:\nCargo.toml\n[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nwasm-bindgen = \"0.2\"\nThe dependency is the familiar part.\nThe [lib] section is new. It tells Cargo what shape to build the library in.\nrlib is the shape Rust uses: it is what main.rs and the tests have been linking against all along. cdylib is a shape other languages can load. Think of a front door for the family and a side door for guests. We ask for both, so that cargo run and cargo test keep using the door they know, and wasm-pack gets one it can open.\nBuild:\ncargo build\nCargo downloads wasm-bindgen and a few helpers, compiles them, and finishes with nothing to say. Nothing is marked yet.\nRun cargo test too. Twenty-eight.\nDo not export the engine\nHere is the tempting move: put the wasm-bindgen attribute on Game, on Player, on Enemy, and let JavaScript have everything.\nWe are not going to do that, for the same reason rules is not on the front door.\nGame has never heard of a browser. The moment it carries a browser attribute, it has. Every future change to the engine would have to ask whether JavaScript still likes the shape. And JavaScript would be free to reach past the methods, read the fields, and start doing arithmetic of its own.\nSo we build an adapter: a small type that holds a Game, and shows JavaScript only what we choose.\nIt is a separate concern, so it gets a separate module. Create src/web.rs:\nsrc/web.rs\nuse crate::game::Game;\nuse wasm_bindgen::prelude::*;\n#[wasm_bindgen(js_name = Game)]\npub struct WebGame {\ninner: Game,\n}\n#[wasm_bindgen(js_class = Game)]\nimpl WebGame {\n#[wasm_bindgen(constructor)]\npub fn new() -> WebGame {\nWebGame { inner: Game::new() }\n}\npub fn click(&mut self) {\nself.inner.click();\n}\npub fn tick(&mut self, seconds: u32) {\nself.inner.tick(seconds);\n}\n}\nAnd register it. In src/lib.rs, add mod web; under mod save;. It does not go on the front door. JavaScript will not come in through lib.rs; it comes in through the glue.\nNow read web.rs from the top.\nuse wasm_bindgen::prelude::*; brings in everything the crate expects us to use. A prelude is a crate's \"you will want all of these\" list. The * means all of it.\n#[wasm_bindgen] is an attribute, like #[derive(...)] and #[test]: a note to the compiler, attached to the thing below it. This one says JavaScript should be able to see this.\nWebGame is the adapter. It has one field, inner, holding the real Game. The field is private. JavaScript cannot see inside, and neither can anything outside this file.\njs_name = Game on the struct, and js_class = Game on the impl block, say what the type is called on the JavaScript side. In Rust it is WebGame, so the two never collide. In the browser it is Game, because that is what it is.\n#[wasm_bindgen(constructor)] marks new() as the thing new Game() runs in JavaScript.\nclick() and tick() are the smallest methods in the project. They hand the call to inner and that is all. That is what an adapter does. The rules stay on the far side.\nQuestion: Why does WebGame hold a private Game instead of exporting every engine field?\nPrivate fields make WebAssembly download faster\nThe adapter keeps JavaScript's interface small and leaves the rules in Rust\nRust structs cannot contain public fields\nAnswer: 2. The adapter keeps JavaScript's interface small and leaves the rules in Rust\nExplanation: JavaScript gets a few operations chosen by the adapter. It cannot reach into the engine and start calculating a second version of a rule.\nBuild it:\ncargo build\nIt compiles with nothing to say. On your own machine, cargo build still reads the attribute and checks that everything it marks could cross to JavaScript, which is about to matter. But it exports nothing. The result is a normal library, which is why cargo test keeps working, and we will lean on that before we are done.\nOnly some things can cross\nbuy_click_upgrade() is next. The engine's version returns a PurchaseOutcome. Try handing that straight across. Add this inside the impl WebGame block:\nsrc/web.rs\n#[wasm_bindgen(js_name = buyClickUpgrade)]\npub fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.inner.buy_click_upgrade()\n}\nAnd add use crate::player::PurchaseOutcome; under the first use line.\nThe js_name attribute works on methods too. Rust calls it buy_click_upgrade, because that is how Rust names things. JavaScript calls it buyClickUpgrade, because that is how JavaScript names things. Each side gets its own convention and neither has to flinch.\nBuild, and Rust refuses, because this type is not allowed across:\nerror[E0277]: the trait bound `PurchaseOutcome: IntoWasmAbi` is not satisfied\n--> src/web.rs:10:1\n|\n10 | #[wasm_bindgen(js_class = Game)]\n| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound\n|\nhelp: the trait `IntoWasmAbi` is not implemented for `PurchaseOutcome`\n(There are several of these, and one names a cousin trait, WasmDescribe. They all say the same thing: this type cannot cross.)\nIntoWasmAbi is the trait for can be handed to JavaScript. Numbers have it. bool has it. String and &str have it, thanks to the glue. Types marked with #[wasm_bindgen] have it.\nPurchaseOutcome does not, and we are not going to mark it, because it belongs to the engine.\nSo what crosses instead?\nfalse is still not an answer. We settled that in the seventh article, and a border does not change it. What crosses is the name of the answer, as text:\nsrc/web.rs\n#[wasm_bindgen(js_name = buyClickUpgrade)]\npub fn buy_click_upgrade(&mut self) -> String {\nmatch self.inner.buy_click_upgrade() {\nPurchaseOutcome::Purchased => String::from(\"purchased\"),\nPurchaseOutcome::NotEnoughGold => String::from(\"notEnoughGold\"),\n}\n}\nReplace the version you just wrote with this one.\nThe match is doing the translating. Rust's enum stays an enum. JavaScript gets a string it can compare against. If a third outcome ever appears, the match refuses to compile until it has a name too, which is the same protection we had before.\nQuestion: What crosses into JavaScript when an upgrade cannot be bought?\nThe whole PurchaseOutcome enum definition\nfalse\nThe string \"notEnoughGold\"\nAnswer: 3. The string \"notEnoughGold\"\nExplanation: The adapter translates the Rust enum variant into text. The engine keeps the enum, including Rust's exhaustive match check.\nBuild, and it is quiet again.\nA snapshot for the screen\nIf JavaScript ever computes click damage, there are two programs that know how, and one day they will disagree.\nThat is the rule this whole section protects.\nThe browser is going to draw a screen, and a screen needs numbers: the stage, the gold, how much a click does, what the next upgrade costs. Some of those are fields. Some are rules. We could hand JavaScript the fields and let it work out the rest, and that is exactly the thing it must never do.\nSo the adapter answers all of it at once, as one piece of JSON, and Rust does every bit of arithmetic before the text leaves.\nAdd this at the bottom of web.rs, after the impl WebGame block:\nsrc/web.rs\n#[derive(Serialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct Snapshot {\nstage: u32,\ngold: u32,\nxp: u32,\nxp_needed: u32,\nlevel: u32,\nclick_damage: u32,\nclick_upgrade_level: u32,\nclick_upgrade_cost: u32,\nenemy_name: String,\nenemy_health: u32,\nenemy_max_health: u32,\n}\nimpl Snapshot {\nfn of(game: &Game) -> Snapshot {\nSnapshot {\nstage: game.stage,\ngold: game.player.gold,\nxp: game.player.xp,\nxp_needed: game.player.xp_needed(),\nlevel: game.player.level,\nclick_damage: game.player.click_damage(),\nclick_upgrade_level: game.player.click_upgrade_level,\nclick_upgrade_cost: game.player.click_upgrade_cost(),\nenemy_name: game.enemy.name.clone(),\nenemy_health: game.enemy.health,\nenemy_max_health: enemy_health_for_stage(game.stage),\n}\n}\n}\nAnd at the top of the file, two more use lines, under the one for PurchaseOutcome:\nsrc/web.rs\nuse crate::rules::enemy_health_for_stage;\nuse serde::Serialize;\nA Snapshot is a flat list of everything a screen might want, taken at one moment. It only needs Serialize, not Deserialize, because nobody ever reads a snapshot back in. It is a picture, not a save.\nThree lines in of() deserve a look.\nxp_needed, click_damage and click_upgrade_cost are not copied. They are asked for. Each one is a rule, applied in Rust, and only the answer goes in the picture.\nenemy_max_health is the same idea. The enemy does not store its full health; the rule enemy_health_for_stage() knows it, and the stage is right there.\ngame.enemy.name.clone() is new. A String owns its text, and two things cannot own the same text at once. The enemy owns that name. The snapshot wants its own, so .clone() makes a second copy. (The u32 fields do not need this. A number is small enough that Rust copies it without asking.)\nAnd the attribute above the struct:\nsrc/web.rs\n#[serde(rename_all = \"camelCase\")]\nThis is the article where JavaScript finally reads our JSON, so this is the article where the names change shape. Rust writes click_damage. JavaScript expects clickDamage. rename_all tells serde to convert every field name on the way out, and the Rust code stays the way Rust likes it.\nThe save is not getting this. JavaScript stores a save and hands it back; it never looks inside. Changing the save's field names would be a new save version for the benefit of nobody.\nTwo small things remain before it builds.\nclick_upgrade_cost() does not exist on Player yet. The cost has only ever been needed inside a purchase. Now a screen wants to show it. In src/player.rs, add a method under click_damage():\nsrc/player.rs\npub fn click_upgrade_cost(&self) -> u32 {\nclick_upgrade_cost(self.click_upgrade_level)\n}\nAnd since the player can answer that now, buy_click_upgrade() should ask instead of reaching for the rule. Change its first line from:\nsrc/player.rs\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nto:\nsrc/player.rs\nlet cost = self.click_upgrade_cost();\nThen the method that hands the picture over. Back in web.rs, inside impl WebGame, under buy_click_upgrade():\nsrc/web.rs\n#[wasm_bindgen(js_name = stateJson)]\npub fn state_json(&self) -> String {\nlet snapshot = Snapshot::of(&self.inner);\nserde_json::to_string(&snapshot).expect(\"a Snapshot can always be written as JSON\")\n}\nThat expect() is the same promise save_json() makes, for the same reason: a struct of numbers and one string cannot fail to become JSON.\nQuestion: Why does the snapshot contain clickDamage instead of making JavaScript calculate it?\nJavaScript cannot add two numbers\nRust should remain the only place that knows the damage rule\nJSON requires every number to be calculated twice\nAnswer: 2. Rust should remain the only place that knows the damage rule\nExplanation: The screen receives the answer it needs. If JavaScript repeated the formula, the two versions could disagree after a later rule change.\nBuild. Quiet.\nSaves cross too\nSaving is already text, so it crosses as it is:\nsrc/web.rs\n#[wasm_bindgen(js_name = saveJson)]\npub fn save_json(&self) -> String {\nself.inner.save_json()\n}\nLoading is the interesting one, because loading can fail, and now the failure has to cross a border.\nTry the obvious thing first:\nsrc/web.rs\n#[wasm_bindgen(js_name = fromJson)]\npub fn from_json(json: &str) -> Result<WebGame, LoadError> {\nmatch Game::from_saved_json(json) {\nOk(inner) => Ok(WebGame { inner }),\nErr(error) => Err(error),\n}\n}\nAdd use crate::save::LoadError; to the use lines, and build. Rust refuses again, and this time the complaint is about the error half:\nerror[E0277]: the trait bound `Result<WebGame, LoadError>: ReturnWasmAbi` is not satisfied\n--> src/web.rs:13:1\n|\n13 | #[wasm_bindgen(js_class = Game)]\n| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `ReturnWasmAbi` is not implemented for `Result<WebGame, LoadError>`\n|\nhelp: the trait `ReturnWasmAbi` is implemented for `Result<T, E>`\n|\n779 | / impl<T, E> ReturnWasmAbi for Result<T, E>\n780 | | where\n781 | |     T: IntoWasmAbi,\n782 | |     E: Into<JsValue>,\nSame story as the purchase, with one new piece of information at the bottom.\nA Result can cross, as long as both halves can. WebGame is fine. LoadError is not, and the help says what the error half needs to be: something that can become a JsValue, which is the crate's word for any JavaScript value.\nA String can. So the adapter translates the error the same way it translated the purchase: into words.\nsrc/web.rs\n#[wasm_bindgen(js_name = fromJson)]\npub fn from_json(json: &str) -> Result<WebGame, String> {\nmatch Game::from_saved_json(json) {\nOk(inner) => Ok(WebGame { inner }),\nErr(LoadError::Unreadable) => Err(String::from(\"That save could not be read.\")),\nErr(LoadError::UnsupportedVersion(version)) => Err(format!(\n\"That save is version {version}, and this game cannot read it.\"\n)),\n}\n}\nmain() prints a failed load with {error:?}, which shows the variant's name: Could not load the save: Unreadable. That is fine in a terminal only we read. A player deserves a sentence, so the adapter writes one for each way a load can fail.\nformat!() is println!()'s quieter sibling. Same curly braces, but instead of printing the result, it hands it back as a String.\nWhat does JavaScript see when the Result is an Err? The glue throws it. On the other side, Game.fromJson(text) either returns a game or throws the message, and a try/catch catches it. We will use that next time, when there is a save to load.\nBuild. Quiet. Here is web.rs so far, for checking against. The Snapshot code at the bottom is unchanged:\nsrc/web.rs\nuse crate::game::Game;\nuse crate::player::PurchaseOutcome;\nuse crate::rules::enemy_health_for_stage;\nuse crate::save::LoadError;\nuse serde::Serialize;\nuse wasm_bindgen::prelude::*;\n#[wasm_bindgen(js_name = Game)]\npub struct WebGame {\ninner: Game,\n}\n#[wasm_bindgen(js_class = Game)]\nimpl WebGame {\n#[wasm_bindgen(constructor)]\npub fn new() -> WebGame {\nWebGame { inner: Game::new() }\n}\npub fn click(&mut self) {\nself.inner.click();\n}\npub fn tick(&mut self, seconds: u32) {\nself.inner.tick(seconds);\n}\n#[wasm_bindgen(js_name = buyClickUpgrade)]\npub fn buy_click_upgrade(&mut self) -> String {\nmatch self.inner.buy_click_upgrade() {\nPurchaseOutcome::Purchased => String::from(\"purchased\"),\nPurchaseOutcome::NotEnoughGold => String::from(\"notEnoughGold\"),\n}\n}\n#[wasm_bindgen(js_name = stateJson)]\npub fn state_json(&self) -> String {\nlet snapshot = Snapshot::of(&self.inner);\nserde_json::to_string(&snapshot).expect(\"a Snapshot can always be written as JSON\")\n}\n#[wasm_bindgen(js_name = saveJson)]\npub fn save_json(&self) -> String {\nself.inner.save_json()\n}\n#[wasm_bindgen(js_name = fromJson)]\npub fn from_json(json: &str) -> Result<WebGame, String> {\nmatch Game::from_saved_json(json) {\nOk(inner) => Ok(WebGame { inner }),\nErr(LoadError::Unreadable) => Err(String::from(\"That save could not be read.\")),\nErr(LoadError::UnsupportedVersion(version)) => Err(format!(\n\"That save is version {version}, and this game cannot read it.\"\n)),\n}\n}\n}\nSeven things JavaScript can do. That is the whole border.\nTest it where you are\nWe have not compiled for WebAssembly yet, and we can already test the adapter, because on this machine it is ordinary Rust.\nAdd a tests module at the bottom of web.rs:\nsrc/web.rs\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn a_new_game_reports_its_starting_state() {\nlet game = WebGame::new();\nassert_eq!(\ngame.state_json(),\nr#\"{\"stage\":1,\"gold\":0,\"xp\":0,\"xpNeeded\":20,\"level\":1,\"clickDamage\":5,\"clickUpgradeLevel\":0,\"clickUpgradeCost\":10,\"enemyName\":\"Training Slime\",\"enemyHealth\":20,\"enemyMaxHealth\":20}\"#\n);\n}\n#[test]\nfn a_purchase_answers_in_words() {\nlet mut game = WebGame::new();\nassert_eq!(game.buy_click_upgrade(), \"notEnoughGold\");\ngame.tick(100);\nassert_eq!(game.buy_click_upgrade(), \"purchased\");\n}\n#[test]\nfn a_save_crosses_the_border_and_back() {\nlet mut game = WebGame::new();\ngame.tick(600);\nlet save = game.save_json();\nlet restored = WebGame::from_json(&save).expect(\"the save should load\");\nassert_eq!(restored.state_json(), game.state_json());\n}\n#[test]\nfn nonsense_is_refused_at_the_border() {\nassert_eq!(\nWebGame::from_json(\"banana\").err(),\nSome(String::from(\"That save could not be read.\"))\n);\n}\n}\nThe first test is the one to read slowly. That is the exact text a browser will receive for a new game: every name in camelCase, every rule already applied. Click damage is 5 because level 1 hits for 5. The upgrade costs 10. The slime has 20 of 20.\nThe last test uses .err(), which turns a Result into an Option: Some(error) if it failed, None if it did not. We cannot assert_eq! on the whole Result, because WebGame has no PartialEq and does not need one. Comparing just the error half is enough.\nRun them:\ncargo test\nThirty-two. Thirty in the library, and the two outside tests.\nIf you see twenty-eight, the mod web; line is missing from lib.rs. Without it, the file is not part of the library at all, and nothing complains until much later, in the browser. Add the line and run the tests again.\nBuild for the browser\nNow the other machine.\nwasm-pack build --target web --out-dir web/pkg\n--target web asks for glue that a plain web page can import, with no bundler and no Node. --out-dir web/pkg says where to put it.\nThe first run is slow. wasm-pack fetches the wasm-bindgen command-line program to match the crate version in Cargo.lock, compiles the library for wasm32-unknown-unknown in release mode, runs the glue generator, and shrinks the result. After that it is a few seconds.\nWhen it finishes, the last lines look like this:\n[INFO]: Optimizing wasm binaries with `wasm-opt`...\n[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended\n[INFO]: :-) Done in 24.46s\n[INFO]: :-) Your wasm pkg is ready to publish at /home/you/rustbound-idle/web/pkg.\nYour time and path will differ, and the note about optional fields is safe to ignore. Look in the folder:\nweb/pkg/\n├── .gitignore\n├── package.json\n├── rustbound_idle.d.ts\n├── rustbound_idle.js\n├── rustbound_idle_bg.wasm\n└── rustbound_idle_bg.wasm.d.ts\nrustbound_idle_bg.wasm is the compiled game, about 110 KB. (We will look at that number again when it is time to ship.) rustbound_idle.js is the glue: the JavaScript that loads the .wasm file and wraps every marked method so that strings go in and out as strings. package.json describes the folder to JavaScript tooling we are not using.\nThe two .d.ts files are the ones to open. They are written for TypeScript, a version of JavaScript with types, but you do not need to know TypeScript to read this; it is a list of everything JavaScript can call, and it is the clearest summary of what we just built. The Game class is near the top of rustbound_idle.d.ts; everything below it is glue you can ignore:\nweb/pkg/rustbound_idle.d.ts\nexport class Game {\nfree(): void;\n[Symbol.dispose](): void;\nbuyClickUpgrade(): string;\nclick(): void;\nstatic fromJson(json: string): Game;\nconstructor();\nsaveJson(): string;\nstateJson(): string;\ntick(seconds: number): void;\n}\nEvery js_name we chose is in there. fromJson is static, because it makes a Game rather than being called on one, which is the same reason from_saved_json() was an associated function.\nfree() and [Symbol.dispose] are the two we did not write. A Game on the Rust side lives in WebAssembly's memory, and JavaScript's garbage collector cannot see in there. free() is how JavaScript gives a game back when it is done with it, and the other is a newer spelling of the same thing. We will not need either today.\nThe .gitignore inside pkg/ contains a single *. The whole folder is generated, like target/, and it stays out of version control.\nIf instead you see this:\nError: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\nthen the [lib] section did not make it into Cargo.toml. Add it and run the build again.\nAnd if the build succeeds but the .d.ts file has no Game class in it, mod web; is missing from lib.rs. The page will later complain that the module \"does not provide an export named 'Game'\", which is the same problem seen from the other side.\nA page that runs the engine\nCreate web/index.html, next to the pkg folder:\nweb/index.html\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<title>Rustbound Idle</title>\n</head>\n<body>\n<h1>Rustbound Idle</h1>\n<pre id=\"output\">Loading the engine...</pre>\n<script type=\"module\">\nimport init, { Game } from \"./pkg/rustbound_idle.js\";\nawait init();\nconst game = new Game();\nconst before = JSON.parse(game.stateJson());\ngame.tick(3600);\nconst after = JSON.parse(game.stateJson());\ndocument.querySelector(\"#output\").textContent =\n`One hour away.\\nStage ${before.stage} -> ${after.stage}  ` +\n`Level ${before.level} -> ${after.level}  ` +\n`Gold ${before.gold} -> ${after.gold}`;\n</script>\n</body>\n</html>\nThe script is the whole point, so read it line by line.\nimport init, { Game } from \"./pkg/rustbound_idle.js\"; brings in two things from the glue. Game is our class. init is a function that fetches rustbound_idle_bg.wasm and loads it into the browser.\nawait init(); does that loading. It is the one step that takes time, which is why the page says \"Loading the engine...\" until it is done. (If you have been told await only works inside an async function: at the top of a <script type=\"module\"> it is allowed. That is one of the things type=\"module\" buys us.)\nnew Game() runs our constructor. game.stateJson() runs our snapshot. game.tick(3600) runs the same loop that has run in every test since the eighth article.\nJSON.parse() turns our text into a JavaScript object, so before.stage and before.gold just work. The names are camelCase, and that is rename_all paying off.\nThe sandbox below exercises only that browser-side handoff. It does not pretend to compile or load the generated .wasm package. Run it once, then change the three values in after and run it again. The sentence should follow the snapshot without learning a single game rule.\nHTML\n<h2>Rustbound Idle</h2>\n<pre id=\"output\">Waiting for a snapshot...</pre>\nJavaScript\nconst snapshotText = '{\"stage\":1,\"gold\":0,\"xp\":0,\"xpNeeded\":20,\"level\":1,\"clickDamage\":5,\"clickUpgradeLevel\":0,\"clickUpgradeCost\":10,\"enemyName\":\"Training Slime\",\"enemyHealth\":20,\"enemyMaxHealth\":20}';\nconst before = JSON.parse(snapshotText);\nconst after = {\n...before,\nstage: 128,\nlevel: 47,\ngold: 16637,\n};\ndocument.querySelector(\"#output\").textContent =\n`One hour away.\\nStage ${before.stage} -> ${after.stage}  ` +\n`Level ${before.level} -> ${after.level}  ` +\n`Gold ${before.gold} -> ${after.gold}`;\nLive example · render the Rust snapshot on the canonical article (https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/).\nThen the last lines print the same sentence main() prints. Everything else on the page is scaffolding.\nNow serve it. A browser will not load a module or a .wasm file from a plain file on disk; both have to come from a web server, even a trivial one. Python ships with a trivial one:\npython3 -m http.server 8080 --directory web\n(On Windows the command is usually python rather than python3.)\nOpen http://localhost:8080/ in a browser.\nRustbound Idle\nOne hour away.\nStage 1 -> 128  Level 1 -> 47  Gold 0 -> 16637\nStage 128. Level 47.\nThose are the numbers in tests/idle_hour.rs, and they have been since the ninth article. The test runs on your machine, in Rust. The page runs in a browser, in WebAssembly, called from JavaScript. Same library. Same answer.\nIf you open index.html directly instead, by double-clicking it, the page stays on \"Loading the engine...\" and the browser console says something like:\nAccess to script at 'file:///.../web/pkg/rustbound_idle.js' from origin 'null'\nhas been blocked by CORS policy: Cross origin requests are only supported for\nprotocol schemes: chrome, chrome-untrusted, data, http, https.\nThat is the browser refusing to load modules from file://. It is not a bug in our code. Use the server.\nOne habit to pick up now: after you change the Rust and rebuild, browsers like to keep serving the old .wasm file. If a number on the page does not change when you expected it to, do a hard refresh (Ctrl+Shift+R, or Cmd+Shift+R on a Mac) before you go looking for a bug.\nOne more thing to notice: cargo run still works. Try it. The terminal version prints its three stages and its hour, exactly as before. We did not replace a program. We gave the same library a second door.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along, here is everything that changed. src/rules.rs, src/enemy.rs, src/game.rs, src/save.rs, src/main.rs, and the two files in tests/ are exactly as they were in the starting point.\nThe project now looks like this:\nrustbound-idle/\n├── Cargo.toml\n├── Cargo.lock\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   ├── game.rs\n│   ├── save.rs\n│   └── web.rs\n├── tests/\n│   ├── idle_hour.rs\n│   └── save_and_restore.rs\n└── web/\n├── index.html\n└── pkg/            (generated by wasm-pack)\nCargo.toml:\nCargo.toml\n[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[lib]\ncrate-type = [\"cdylib\", \"rlib\"]\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nwasm-bindgen = \"0.2\"\nsrc/lib.rs:\nsrc/lib.rs\nmod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\nmod web;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use save::LoadError;\nOnly the impl block in src/player.rs changes; the struct, the enum, and the tests are as they were.\n<details>\n<summary><code>src/player.rs</code> · changed 56-line <code>impl</code></summary>\nsrc/player.rs\nimpl Player {\npub(crate) fn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\npub(crate) fn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\npub(crate) fn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\n}\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(self.base_damage(), self.bonus_damage())\n}\npub fn click_upgrade_cost(&self) -> u32 {\nclick_upgrade_cost(self.click_upgrade_level)\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\npub(crate) fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = self.click_upgrade_cost();\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\n</details>\n<details>\n<summary><code>src/web.rs</code> · 137 lines</summary>\nsrc/web.rs\nuse crate::game::Game;\nuse crate::player::PurchaseOutcome;\nuse crate::rules::enemy_health_for_stage;\nuse crate::save::LoadError;\nuse serde::Serialize;\nuse wasm_bindgen::prelude::*;\n#[wasm_bindgen(js_name = Game)]\npub struct WebGame {\ninner: Game,\n}\n#[wasm_bindgen(js_class = Game)]\nimpl WebGame {\n#[wasm_bindgen(constructor)]\npub fn new() -> WebGame {\nWebGame { inner: Game::new() }\n}\npub fn click(&mut self) {\nself.inner.click();\n}\npub fn tick(&mut self, seconds: u32) {\nself.inner.tick(seconds);\n}\n#[wasm_bindgen(js_name = buyClickUpgrade)]\npub fn buy_click_upgrade(&mut self) -> String {\nmatch self.inner.buy_click_upgrade() {\nPurchaseOutcome::Purchased => String::from(\"purchased\"),\nPurchaseOutcome::NotEnoughGold => String::from(\"notEnoughGold\"),\n}\n}\n#[wasm_bindgen(js_name = stateJson)]\npub fn state_json(&self) -> String {\nlet snapshot = Snapshot::of(&self.inner);\nserde_json::to_string(&snapshot).expect(\"a Snapshot can always be written as JSON\")\n}\n#[wasm_bindgen(js_name = saveJson)]\npub fn save_json(&self) -> String {\nself.inner.save_json()\n}\n#[wasm_bindgen(js_name = fromJson)]\npub fn from_json(json: &str) -> Result<WebGame, String> {\nmatch Game::from_saved_json(json) {\nOk(inner) => Ok(WebGame { inner }),\nErr(LoadError::Unreadable) => Err(String::from(\"That save could not be read.\")),\nErr(LoadError::UnsupportedVersion(version)) => Err(format!(\n\"That save is version {version}, and this game cannot read it.\"\n)),\n}\n}\n}\n#[derive(Serialize)]\n#[serde(rename_all = \"camelCase\")]\nstruct Snapshot {\nstage: u32,\ngold: u32,\nxp: u32,\nxp_needed: u32,\nlevel: u32,\nclick_damage: u32,\nclick_upgrade_level: u32,\nclick_upgrade_cost: u32,\nenemy_name: String,\nenemy_health: u32,\nenemy_max_health: u32,\n}\nimpl Snapshot {\nfn of(game: &Game) -> Snapshot {\nSnapshot {\nstage: game.stage,\ngold: game.player.gold,\nxp: game.player.xp,\nxp_needed: game.player.xp_needed(),\nlevel: game.player.level,\nclick_damage: game.player.click_damage(),\nclick_upgrade_level: game.player.click_upgrade_level,\nclick_upgrade_cost: game.player.click_upgrade_cost(),\nenemy_name: game.enemy.name.clone(),\nenemy_health: game.enemy.health,\nenemy_max_health: enemy_health_for_stage(game.stage),\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn a_new_game_reports_its_starting_state() {\nlet game = WebGame::new();\nassert_eq!(\ngame.state_json(),\nr#\"{\"stage\":1,\"gold\":0,\"xp\":0,\"xpNeeded\":20,\"level\":1,\"clickDamage\":5,\"clickUpgradeLevel\":0,\"clickUpgradeCost\":10,\"enemyName\":\"Training Slime\",\"enemyHealth\":20,\"enemyMaxHealth\":20}\"#\n);\n}\n#[test]\nfn a_purchase_answers_in_words() {\nlet mut game = WebGame::new();\nassert_eq!(game.buy_click_upgrade(), \"notEnoughGold\");\ngame.tick(100);\nassert_eq!(game.buy_click_upgrade(), \"purchased\");\n}\n#[test]\nfn a_save_crosses_the_border_and_back() {\nlet mut game = WebGame::new();\ngame.tick(600);\nlet save = game.save_json();\nlet restored = WebGame::from_json(&save).expect(\"the save should load\");\nassert_eq!(restored.state_json(), game.state_json());\n}\n#[test]\nfn nonsense_is_refused_at_the_border() {\nassert_eq!(\nWebGame::from_json(\"banana\").err(),\nSome(String::from(\"That save could not be read.\"))\n);\n}\n}\n</details>\n<details>\n<summary><code>web/index.html</code> · 29 lines</summary>\nweb/index.html\n<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<title>Rustbound Idle</title>\n</head>\n<body>\n<h1>Rustbound Idle</h1>\n<pre id=\"output\">Loading the engine...</pre>\n<script type=\"module\">\nimport init, { Game } from \"./pkg/rustbound_idle.js\";\nawait init();\nconst game = new Game();\nconst before = JSON.parse(game.stateJson());\ngame.tick(3600);\nconst after = JSON.parse(game.stateJson());\ndocument.querySelector(\"#output\").textContent =\n`One hour away.\\nStage ${before.stage} -> ${after.stage}  ` +\n`Level ${before.level} -> ${after.level}  ` +\n`Gold ${before.gold} -> ${after.gold}`;\n</script>\n</body>\n</html>\n</details>\nYou do not need to add web/pkg to your own .gitignore. The one wasm-pack wrote inside it already keeps the folder out.\nRun all three:\ncargo test\nwasm-pack build --target web --out-dir web/pkg\npython3 -m http.server 8080 --directory web\nThirty-two tests pass, the package builds, and the page at http://localhost:8080/ shows the hour away. Clippy should be quiet.\nWhat we learned\nAlong the way, we encountered:\ncompile targets, and wasm32-unknown-unknown\nwhat WebAssembly can and cannot do on its own\nwasm-bindgen and wasm-pack\ncargo install\n[lib] crate-type, and the difference between rlib and cdylib\n#[wasm_bindgen], js_name, js_class, and constructor\na prelude, and use ...::*\nan adapter that holds the engine instead of exposing it\nwhich types can cross a language border, and translating the ones that cannot\na snapshot that applies every rule before the text leaves Rust\n#[serde(rename_all = \"camelCase\")]\n.clone() for a String\nformat!()\na Result crossing into JavaScript as a return or a throw\n.err() on a Result\ntesting a WebAssembly adapter as plain Rust\nES modules, import, await init(), and why file:// will not do\nYou do not need to memorize all of that yet.\nTwo ideas are worth keeping.\nThe first is that the border is a translation, not a window. Nothing on the far side can see Game. It sees seven operations, each of which hands numbers or text across, and every rule has already been applied by the time the text leaves. The match in buy_click_upgrade() and the match in from_json() are the only places the two worlds touch, and both of them turn a Rust enum into words.\nThe second is that the hard part was already done. This article added one file to the library, one line to lib.rs, a four-line method to player.rs, and four lines of configuration. Everything else was two articles ago, when we decided main() should not know any formulas, and last time, when we decided a save should be text. A library that never printed, never asked the time, and never opened a file turned out to be a library that compiles for a machine with none of those things.\nThat is what the wall was for.\nSomething to click\nOur engine runs in a browser now. Nobody can play it.\nThere is no button. There is no screen. There is a <pre> tag and a number.\nNext time, we put a face on it: one custom HTML element, <idle-game>, that draws the snapshot, turns a click on a button into game.click(), keeps the clock, and saves to the browser's storage so that closing the tab is not game over either.\nThe browser will own time and storage and pixels.\nIt still will not get the rules.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/#ob-discussion)",
      "date_published": "2026-10-30T18:00:00Z",
      "id": "https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/",
      "image": "https://mnixblog.netlify.app/images/rust-game-webassembly-wasm-bindgen/cover.jpg",
      "summary": "Compile a Rust idle-game engine to WebAssembly, give JavaScript a small adapter instead of the rules, and run the same tested game in a browser.",
      "tags": [
        "Rust",
        "WebAssembly",
        "JavaScript",
        "Game Development",
        "Beginners"
      ],
      "title": "JavaScript Does Not Get the Rules.",
      "url": "https://mnixblog.netlify.app/rust-game-webassembly-wasm-bindgen/"
    },
    {
      "content_html": "<p>Our game can simulate an hour without making us wait an hour.</p>\n<p>It cannot survive the program ending.</p>\n<p>Close the terminal and everything is gone: the stages, the gold, the levels, the upgrade you bought. The next <code>cargo run</code> starts at stage 1 with a fresh slime, as if nothing ever happened.</p>\n<p>An idle game that forgets you the moment you leave is not much of an idle game.</p>\n<p>I learned what lost progress feels like on a 386, building DOOM maps. It crashed in the middle of one and took an entire afternoon of carefully placed monsters and secret rooms with it. Nothing has taught me to save my work quite like that afternoon.</p>\n<p>Our game deserves better than my DOOM maps got.</p>\n<p>Today the game learns to write itself down, and to read itself back.</p>\n<p>Not to a file, or a browser, or a server. The library will not know where saves go, and it should not. It will only know how to say, in text:</p>\n<blockquote>\n<p>This is everything about me.</p>\n</blockquote>\n<p>And, handed that same text later:</p>\n<blockquote>\n<p>I remember.</p>\n</blockquote>\n<p>By the end, <code>main()</code> clears three stages, saves, throws the game away, loads the save, and <em>then</em> spends the hour away:</p>\n<pre><code class=\"language-text\">Saved: {\"save_version\":1,\"stage\":4,\"player\":{\"gold\":11,\"xp\":4,\"level\":2,\"click_upgrade_level\":1},\"enemy_health\":50}\nLoaded. Stage 4, level 2.\nOne hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<p>Same hour. Same numbers. A different game object, rebuilt from a line of text.</p>\n<p>Saving always works. Loading might not: the text could be anything. So Rust hands the loaded game back in a box that holds one of two things, the game or the reason there is no game, and will not let us reach for the game until we have said what happens if it is the other one.</p>\n<p>Along the way we add our first dependency, and that box turns out to be how Rust handles everything that can fail. There are five quick knowledge checks, two live examples you can edit and run in the page, and a coding challenge.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>This series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.</p>\n<p>If you are only reading along, skip ahead to \"A save is just text.\" The code is folded away below, one file at a time.</p>\n<p>If you are building along, you need the library we built last time: a project called <code>rustbound-idle</code> with six source files and one outside test. (If you kept your project from the last article, you already have all of this. The <code>article_9</code> branch of the companion repository has it too.)</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   └── game.rs\n└── tests/\n    └── idle_hour.rs\n</code></pre>\n<p><code>Cargo.toml</code> is whatever <code>cargo new rustbound-idle</code> gave you. It should say <code>edition = \"2024\"</code>, which needs Rust 1.85 or newer.</p>\n<details>\n<summary><code>src/lib.rs</code> · 8 lines</summary>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</code></pre>\n</details>\n<details>\n<summary><code>src/main.rs</code> · 56 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                game.player.xp_needed()\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    game.player.click_damage()\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                game.player.click_damage(),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/rules.rs</code> · 105 lines</summary>\n<pre><code class=\"language-rust\">pub(crate) fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\npub(crate) fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\npub(crate) fn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\npub(crate) fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\npub(crate) fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\npub(crate) fn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\npub(crate) fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\npub(crate) fn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\npub(crate) fn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\npub(crate) fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\npub(crate) fn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/enemy.rs</code> · 19 lines</summary>\n<pre><code class=\"language-rust\">use crate::rules::{enemy_health_for_stage, gold_reward_for_stage, xp_reward_for_stage};\n\npub struct Enemy {\n    pub name: String,\n    pub health: u32,\n    pub(crate) gold_reward: u32,\n    pub(crate) xp_reward: u32,\n}\n\nimpl Enemy {\n    pub(crate) fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/player.rs</code> · 114 lines</summary>\n<pre><code class=\"language-rust\">use crate::rules::{\n    base_damage_for_level, click_damage, click_upgrade_bonus, click_upgrade_cost, xp_to_next_level,\n};\n\npub struct Player {\n    pub gold: u32,\n    pub xp: u32,\n    pub level: u32,\n    pub click_upgrade_level: u32,\n}\n\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    pub(crate) fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    pub(crate) fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    pub(crate) fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= self.xp_needed() {\n            self.xp -= self.xp_needed();\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    pub fn xp_needed(&amp;self) -&gt; u32 {\n        xp_to_next_level(self.level)\n    }\n\n    pub fn click_damage(&amp;self) -&gt; u32 {\n        click_damage(self.base_damage(), self.bonus_damage())\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    pub(crate) fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/game.rs</code> · 158 lines</summary>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{health_after_hit, idle_damage_for_level, is_defeated};\n\npub struct Game {\n    pub stage: u32,\n    pub player: Player,\n    pub enemy: Enemy,\n}\n\nimpl Game {\n    pub fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    pub fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    pub fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nimpl Default for Game {\n    fn default() -&gt; Game {\n        Game::new()\n    }\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    enemy.health = health_after_hit(enemy.health, player.click_damage());\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(long.enemy.health, short.enemy.health);\n        assert_eq!(long.player.gold, short.player.gold);\n        assert_eq!(long.player.xp, short.player.xp);\n        assert_eq!(long.player.level, short.player.level);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>tests/idle_hour.rs</code> · 11 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn an_hour_away_moves_the_game_forward() {\n    let mut game = Game::new();\n\n    game.tick(3600);\n\n    assert_eq!(game.stage, 128);\n    assert_eq!(game.player.level, 47);\n}\n</code></pre>\n</details>\n<p>Run <code>cargo test</code>, and twenty-two tests pass: twenty-one inside the library and one in <code>tests/idle_hour.rs</code>. Run <code>cargo run</code>, and the player clears three stages, buys an upgrade, and spends an hour away.</p>\n<p>Hold on to twenty-two. Today we add six.</p>\n<hr />\n<h2 id=\"a-save-is-just-text\">A save is just text<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#a-save-is-just-text\" aria-label=\"Link to this section\">#</a></h2>\n<p>What is a save, exactly?</p>\n<p>Strip away the menus and the little floppy-disk icon, and a save is a description of the game, written down somewhere that outlives the program.</p>\n<p>That icon was a real object once. Our house had hundreds of them, the 5.25-inch kind that actually flopped, full of games and programs. They are probably still boxed up somewhere at my parents' house. The idea outlived them.</p>\n<p>For our game, \"the game\" is one <code>Game</code> value: a stage, a player, and the slime they are fighting. Write those numbers down and you have a save. Read them back into a <code>Game</code> and you have loaded it.</p>\n<p>So the question for today is: write them down <em>how</em>?</p>\n<p>We could invent a format. One number per line, in a fixed order, and a parser that reads them back:</p>\n<pre><code class=\"language-text\">4\n11\n4\n2\n1\n50\n</code></pre>\n<p>It would work, until the day we add a field in the middle and every old save quietly reads the wrong number into the wrong slot. Nothing crashes. The player just loads in with fifty gold and an upgrade level of eleven.</p>\n<p>There is a better answer, and most programs that save anything already use it.</p>\n<p>JSON is a plain-text format for describing values with names. Here is a brand-new game, written as JSON:</p>\n<pre><code class=\"language-json\">{\n  \"save_version\": 1,\n  \"stage\": 1,\n  \"player\": {\n    \"gold\": 0,\n    \"xp\": 0,\n    \"level\": 1,\n    \"click_upgrade_level\": 0\n  },\n  \"enemy_health\": 20\n}\n</code></pre>\n<p>Every value has a name, and values can nest: <code>player</code> is a group of four values inside the bigger one.</p>\n<p>Notice that this is <strong>not</strong> a copy of every field in <code>Game</code>. There is no enemy name and no reward. It is the minimum we need to rebuild the same game: the stage, the player's progress, and how much health the current slime has left.</p>\n<p>That last one matters more than it looks. Growing up, my brothers and I often played Super Nintendo games on an emulator instead of the console, and a big reason was being able to save anywhere. <code>enemy_health</code> is our version of that. We can save in the middle of a fight, and the slime will still be wounded when we come back.</p>\n<p>The <code>save_version</code> line looks like it does nothing yet. It is there for a future version of us, and we will come back to it.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why save the game as named JSON values instead of one number per line?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>JSON files are always smaller</li>\n<li>Every value carries its name, so a new or reordered field cannot slide into the wrong slot</li>\n<li>Rust cannot read a file of plain numbers</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Every value carries its name, so a new or reordered field cannot slide into the wrong slot</p>\n<p>A fixed order is a promise that nothing will ever change. Names let the loader find <code>gold</code> wherever it is.</p>\n</div>\n</div>\n</section>\n<p>We are not going to write the JSON writer and reader by hand. Someone already has, and it is very good.</p>\n<hr />\n<h2 id=\"the-first-dependency\">The first dependency<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#the-first-dependency\" aria-label=\"Link to this section\">#</a></h2>\n<p>Until now, every line of this game has been ours. Today we borrow some.</p>\n<p>Rust code that other people share is published as crates, and most of them live on <a href=\"https://crates.io\">crates.io</a>. A crate your project uses is called a <strong>dependency</strong>. We are adding two:</p>\n<ul>\n<li><code>serde</code> knows how to take a Rust value apart into named pieces, and put it back together. The name is short for <em>serialize</em> and <em>deserialize</em>, which are the formal words for writing a value down and reading it back.</li>\n<li><code>serde_json</code> knows how to write those pieces as JSON, and read JSON back into pieces.</li>\n</ul>\n<p>Open <code>Cargo.toml</code>. The <code>[dependencies]</code> section has been empty since the first article. Fill it in:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">Cargo.toml</figcaption><pre><code class=\"language-toml\">[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n</code></pre></figure>\n<p><code>\"1\"</code> means <em>any version 1.x</em>. Cargo picks the newest 1.x it can find, and never jumps to a 2.0 on its own.</p>\n<p>The <code>features = [\"derive\"]</code> part matters, and we will see why in a moment.</p>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>The first time, Cargo downloads the two crates and the handful of smaller ones they depend on, and compiles all of them. You will see a dozen <code>Compiling</code> lines you did not write, and then your own:</p>\n<pre><code class=\"language-text\">   Compiling serde v1.0.229\n   Compiling serde_json v1.0.154\n   Compiling serde_derive v1.0.229\n   ...\n   Compiling rustbound-idle v0.1.0\n</code></pre>\n<p>Your version numbers may be newer. That is fine.</p>\n<p>That only happens once. Cargo keeps the compiled crates around, and later builds are as fast as before.</p>\n<p>Look in your project folder and you will find a new file, <code>Cargo.lock</code>. Cargo wrote it. It records the exact versions that got picked, so that anyone who builds this project later gets the same ones. Leave it alone, and if you use Git, commit it.</p>\n<hr />\n<h2 id=\"give-the-save-its-own-shape\">Give the save its own shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#give-the-save-its-own-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>serde</code> can turn Rust values into text and back again, but there is a choice to make first.</p>\n<p>We <em>could</em> teach <code>serde</code> about <code>Game</code>, <code>Player</code>, and <code>Enemy</code> directly.</p>\n<p>That would work.</p>\n<p>It would also make our runtime structs the permanent shape of our save files.</p>\n<p>Those are not the same job.</p>\n<p>A <code>Game</code> is whatever the engine needs while it is running.</p>\n<p>A save is the minimum information needed to rebuild that game later.</p>\n<p>The difference matters because some values can be recalculated from the rules. An enemy's gold and XP rewards come from its stage. If we saved those numbers, a save made today would carry today's rewards into next month's version of the game, even after we rebalanced them.</p>\n<p>So the save gets its own small types.</p>\n<p>Create <code>src/save.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">use serde::{Deserialize, Serialize};\n\nconst SAVE_VERSION: u32 = 1;\n\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct SaveData {\n    save_version: u32,\n    stage: u32,\n    player: PlayerSave,\n    enemy_health: u32,\n}\n</code></pre></figure>\n<p>The two structs are the JSON from before, written as Rust. Same names, same nesting.</p>\n<p><code>const</code> declares a <strong>constant</strong>: a value fixed when the program is compiled, which can never change while it runs. Constants are written in capitals by convention, and they need their type spelled out. <code>SAVE_VERSION</code> is the version of the save format this code writes.</p>\n<p>The interesting line is the one above each struct.</p>\n<h3 id=\"asking-for-a-trait\">Asking for a trait<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#asking-for-a-trait\" aria-label=\"Link to this section\">#</a></h3>\n<p>In plain terms, that line tells Rust: <em>these two structs can be written down, and read back.</em></p>\n<p><code>#[derive(Serialize, Deserialize)]</code> is the same kind of line as the <code>#[derive(Debug, PartialEq)]</code> we put on <code>PurchaseOutcome</code>.</p>\n<p><code>Serialize</code> and <code>Deserialize</code> are <strong>traits</strong>. A trait is a named ability a type can have. <code>PartialEq</code> is the ability to be compared with <code>==</code>. <code>Serialize</code> is the ability to be taken apart into named pieces. <code>Deserialize</code> is the ability to be built back up from them.</p>\n<p><code>derive</code> asks the compiler to write that ability for us, by looking at the fields. We could write it by hand. Nobody wants to. The thing that does the writing is called a <strong>derive macro</strong>; a macro is code that writes code.</p>\n<p>That is also what <code>features = [\"derive\"]</code> was for. Without it, <code>serde</code> gives us the traits but not the code that derives them, and the build stops on every struct:</p>\n<pre><code class=\"language-text\">error: cannot find derive macro `Serialize` in this scope\n  --&gt; src\\save.rs:5:10\n   |\n 5 | #[derive(Serialize, Deserialize)]\n   |          ^^^^^^^^^\n   |\nnote: `Serialize` is imported here, but it is only a trait, without a derive macro\n</code></pre>\n<p><em>Only a trait, without a derive macro.</em> The ability exists; the machine that writes it for us was left out.</p>\n<p>Last, the library has to know the new file exists. Add <code>mod save;</code> to <code>src/lib.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/lib.rs</figcaption><pre><code class=\"language-rust\" data-highlight-lines=\"5\">mod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</code></pre></figure>\n<p>If you build now, Cargo warns that <code>SAVE_VERSION</code>, <code>PlayerSave</code>, and <code>SaveData</code> are never used. Fair. The next section uses them.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does the save store the stage but not the enemy's <code>gold_reward</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>gold_reward</code> is <code>pub(crate)</code>, so <code>serde</code> cannot see it</li>\n<li>JSON cannot hold more than six numbers</li>\n<li>The reward can be rebuilt from the stage, so an old save cannot carry old balance rules into a newer game</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. The reward can be rebuilt from the stage, so an old save cannot carry old balance rules into a newer game</p>\n<p>Save what only the player could have done. Rebuild everything else from the current rules.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"writing-it-down\">Writing it down<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#writing-it-down\" aria-label=\"Link to this section\">#</a></h2>\n<p>Saving is the easy direction. Every <code>Game</code> can be described, so <code>save_json()</code> always gets an answer.</p>\n<p>Add an <code>impl Game</code> block to <code>src/save.rs</code>, below the structs. It is a second <code>impl</code> block for <code>Game</code>, in a different file from the first, and Rust is fine with that: an impl block can live anywhere in the crate that owns the type.</p>\n<p>First, one more import at the top of the file, beside the <code>serde</code> one:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">use crate::game::Game;\n</code></pre></figure>\n<p>Then the block itself:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">impl Game {\n    pub fn save_json(&amp;self) -&gt; String {\n        let save = SaveData {\n            save_version: SAVE_VERSION,\n            stage: self.stage,\n            player: PlayerSave {\n                gold: self.player.gold,\n                xp: self.player.xp,\n                level: self.player.level,\n                click_upgrade_level: self.player.click_upgrade_level,\n            },\n            enemy_health: self.enemy.health,\n        };\n\n        serde_json::to_string(&amp;save).expect(\"a save made of numbers should always serialize\")\n    }\n}\n</code></pre></figure>\n<p>The method copies the numbers that matter out of the running game into a <code>SaveData</code>, and hands it to <code>serde_json::to_string</code>.</p>\n<p>That last line has a word we have not seen.</p>\n<p><code>serde_json::to_string</code> does not hand back a <code>String</code>. It hands back a <strong><code>Result</code></strong>: either <code>Ok</code>, holding the text, or <code>Err</code>, holding what went wrong. It has to, because some Rust values cannot be written as JSON at all.</p>\n<p>Ours can. A struct full of numbers always can. So we call <code>.expect()</code>, which says:</p>\n<blockquote>\n<p>Give me what is inside the <code>Ok</code>. If it is somehow an <code>Err</code>, stop the program with this message.</p>\n</blockquote>\n<p><code>expect()</code> is for the cases where failure would mean a bug in our own code, not bad luck. We will meet a case that is pure bad luck in a moment, and there we will not use it.</p>\n<p>Now a test that pins the format down. Add it to the bottom of <code>src/save.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn a_new_game_saves_its_starting_numbers() {\n        let json = Game::new().save_json();\n\n        assert_eq!(\n            json,\n            r#\"{\"save_version\":1,\"stage\":1,\"player\":{\"gold\":0,\"xp\":0,\"level\":1,\"click_upgrade_level\":0},\"enemy_health\":20}\"#\n        );\n    }\n}\n</code></pre></figure>\n<p>That string starts with <code>r#\"</code> and ends with <code>\"#</code>. It is a <strong>raw string</strong>. JSON is full of double quotes, and in a normal string every one of them would need a backslash in front of it. In a raw string, a <code>\"</code> is just a character, and only <code>\"#</code> ends it.</p>\n<p><code>serde_json::to_string</code> writes everything on one line with no spaces, which is why the expected text is one long line.</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-three tests pass.</p>\n<hr />\n<h2 id=\"reading-it-back\">Reading it back<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#reading-it-back\" aria-label=\"Link to this section\">#</a></h2>\n<p>Loading is the hard direction.</p>\n<p>Saving starts from a <code>Game</code>, which is always valid. Loading starts from text, and text can be anything. A save from an older version. A half-written file. Somebody's grocery list.</p>\n<p>So what should a load function return when the text is not a save?</p>\n<p>A <code>Game</code>? Which one? A brand-new game would quietly wipe out the player's progress, and nothing would tell them why. It is the same trap as the shop that answered <code>false</code> in an earlier lesson: an answer that looks like an answer, where a wrong guess about what it means looks just like success.</p>\n<p>The honest return type is the one <code>serde_json</code> already used on us:</p>\n<pre><code class=\"language-rust\">Result&lt;Game, LoadError&gt;\n</code></pre>\n<blockquote>\n<p>Either a <code>Game</code>, or the reason there is no <code>Game</code>.</p>\n</blockquote>\n<p><code>LoadError</code> is ours to define. It is an enum, like <code>PurchaseOutcome</code>, listing the ways loading can fail. For now there is one. Add it near the top of <code>src/save.rs</code>, below <code>SAVE_VERSION</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">#[derive(Debug, PartialEq)]\npub enum LoadError {\n    Unreadable,\n}\n</code></pre></figure>\n<p>Then the load itself, inside the same <code>impl Game</code> block, below <code>save_json()</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">    pub fn from_saved_json(json: &amp;str) -&gt; Result&lt;Game, LoadError&gt; {\n        let save: SaveData = match serde_json::from_str(json) {\n            Ok(save) =&gt; save,\n            Err(_) =&gt; return Err(LoadError::Unreadable),\n        };\n\n        let mut game = Game::new();\n\n        game.stage = save.stage;\n        game.player.gold = save.player.gold;\n        game.player.xp = save.player.xp;\n        game.player.level = save.player.level;\n        game.player.click_upgrade_level = save.player.click_upgrade_level;\n\n        game.enemy = Enemy::for_stage(save.stage);\n        game.enemy.health = save.enemy_health;\n\n        Ok(game)\n    }\n</code></pre></figure>\n<p>And one more import at the top, because we build an enemy:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">use crate::enemy::Enemy;\n</code></pre></figure>\n<p>Let us walk through it.</p>\n<p><code>json: &amp;str</code> means the function borrows some text instead of taking it. <code>&amp;str</code> is a borrowed piece of text, the way <code>&amp;Player</code> is a borrowed player. Whoever calls <code>from_saved_json()</code> keeps their <code>String</code>.</p>\n<p><code>serde_json::from_str(json)</code> reads the text. It returns a <code>Result</code> too, and this time failure is real: the text might be anything. So we <code>match</code> on it, exactly the way we matched on <code>PurchaseOutcome</code>, one arm per possibility:</p>\n<ul>\n<li><code>Ok(save) =&gt; save</code> takes the <code>SaveData</code> out of the <code>Ok</code> and makes it the value of the whole <code>match</code>.</li>\n<li><code>Err(_) =&gt; return Err(LoadError::Unreadable)</code> ignores the details of what went wrong (that is the <code>_</code>) and leaves the function early with our own error.</li>\n</ul>\n<p>The <code>: SaveData</code> after <code>let save</code> matters more than it looks. <code>from_str</code> can build anything that implements <code>Deserialize</code>. That annotation is how it knows to build a <code>SaveData</code>, and therefore which names to look for.</p>\n<p>The rest rebuilds the game. Start from a new <code>Game</code>, copy the saved progress over it, then ask <code>Enemy::for_stage()</code> for the enemy. That one line is where the save's small shape pays off. The rewards come from the current rules, not from the file. Only the slime's remaining health comes from the save.</p>\n<p>Last, <code>Ok(game)</code> wraps the finished game in the success half of the <code>Result</code>.</p>\n<p>Here is the same round trip on its own. It runs in the page, using the same two crates.</p>\n<p>Run it, and you should see the player saved and loaded. Then break the save on purpose: delete <code>\"level\":2,</code> from the <code>text</code> line and run again. Then put it back and change <code>11</code> to <code>-5</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · break the save on purpose</figcaption><pre><code class=\"language-rust\">use serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nfn main() {\n    let player = PlayerSave {\n        gold: 11,\n        xp: 4,\n        level: 2,\n        click_upgrade_level: 1,\n    };\n\n    let json = serde_json::to_string(&amp;player)\n        .expect(&quot;a save made of numbers should always serialize&quot;);\n    println!(&quot;Saved: {json}&quot;);\n\n    let text = r#&quot;{&quot;gold&quot;:11,&quot;xp&quot;:4,&quot;level&quot;:2,&quot;click_upgrade_level&quot;:1}&quot;#;\n    let loaded: Result&lt;PlayerSave, serde_json::Error&gt; = serde_json::from_str(text);\n\n    match loaded {\n        Ok(player) =&gt; println!(&quot;Loaded: level {} with {} gold&quot;, player.level, player.gold),\n        Err(error) =&gt; println!(&quot;Not a save: {error}&quot;),\n    }\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/\">the original article</a>.</p></figure>\n<p>The first break prints:</p>\n<pre><code class=\"language-text\">Not a save: missing field `level` at line 1 column 42\n</code></pre>\n<p>The second:</p>\n<pre><code class=\"language-text\">Not a save: invalid value: integer `-5`, expected u32 at line 1 column 10\n</code></pre>\n<p><code>serde_json</code> knew exactly what was wrong both times. <code>from_saved_json()</code> throws those details away and says <code>Unreadable</code>, because the game cannot do anything more useful with them. Either way, the <code>Err</code> arm prints and nothing crashes.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>from_saved_json()</code> return <code>Result&lt;Game, LoadError&gt;</code> instead of a <code>Game</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A <code>Result</code> loads faster than a plain struct</li>\n<li>Text that is not a save has no honest <code>Game</code> to return, and the caller has to handle both cases</li>\n<li><code>serde_json</code> only works inside functions that return a <code>Result</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Text that is not a save has no honest <code>Game</code> to return, and the caller has to handle both cases</p>\n<p>Returning <code>Game::new()</code> on failure would look like success and quietly wipe the player's progress.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"rust-will-not-let-us-forget\">Rust will not let us forget<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#rust-will-not-let-us-forget\" aria-label=\"Link to this section\">#</a></h2>\n<p>Time to use it. In <code>src/main.rs</code>, after the <code>while</code> loop and before the hour away, save the game:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/main.rs</figcaption><pre><code class=\"language-rust\">    let save = game.save_json();\n    println!(\"Saved: {save}\");\n</code></pre></figure>\n<p>Then load it back and print where we are. The tempting version is two lines, right under the save lines:</p>\n<pre><code class=\"language-rust\">    let mut game = Game::from_saved_json(&amp;save);\n    println!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\n</code></pre>\n<p>Build that, and Rust refuses, several times over:</p>\n<pre><code class=\"language-text\">error[E0609]: no field `stage` on type `Result&lt;Game, rustbound_idle::save::LoadError&gt;`\n  --&gt; src\\main.rs:49:50\n   |\n49 |     println!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\n   |                                                  ^^^^^ unknown field\n</code></pre>\n<p>The other errors say the same thing about <code>player</code>, and about every later line that touches <code>game</code>.</p>\n<p><code>game</code> is not a <code>Game</code>. It is a <code>Result</code>, and a <code>Result</code> has no stage. The <code>Game</code> is inside it, maybe.</p>\n<p>That is the whole point. Other languages often hand back the game <em>or</em> a special empty value and trust you to check. Rust hands back a box with one of two things in it, and will not let you reach for the game until you have said what happens if it is the other thing.</p>\n<p>So we say it, with a <code>match</code>. Replace the two tempting lines with:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/main.rs</figcaption><pre><code class=\"language-rust\">    let mut game = match Game::from_saved_json(&amp;save) {\n        Ok(game) =&gt; game,\n        Err(error) =&gt; {\n            println!(\"Could not load the save: {error:?}\");\n            return;\n        }\n    };\n\n    println!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\n</code></pre></figure>\n<p>If the load worked, the <code>Game</code> comes out of the <code>Ok</code>. If not, we print the error and <code>return</code> from <code>main()</code>, which ends the program. <code>{error:?}</code> prints the error with <code>Debug</code>, which is why <code>LoadError</code> derives it.</p>\n<p>Anyone outside the library who wants to <code>match</code> on the different kinds of <code>LoadError</code> has to be able to name it. So it goes on the front door, in <code>src/lib.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/lib.rs</figcaption><pre><code class=\"language-rust\">pub use save::LoadError;\n</code></pre></figure>\n<p><code>main.rs</code> does not name it yet. The browser version of the game, a couple of lessons from now, will.</p>\n<hr />\n<h2 id=\"throw-the-game-away\">Throw the game away<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#throw-the-game-away\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at the code we just wrote again. There is something odd about it.</p>\n<p>We already had a <code>game</code>. Then we wrote <code>let mut game = ...</code> a second time.</p>\n<p>That is allowed. A new <code>let</code> with an old name makes a brand-new variable, and the old one can no longer be reached by that name. Rust calls this <strong>shadowing</strong>.</p>\n<p>But the old game is still there, sitting in memory until <code>main()</code> ends. And we want to <em>prove</em> the load works, not just print a game that might secretly be the old one.</p>\n<p>So we throw the old one away first, on purpose, right after saving:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/main.rs</figcaption><pre><code class=\"language-rust\" data-highlight-lines=\"4\">    let save = game.save_json();\n    println!(\"Saved: {save}\");\n\n    drop(game);\n</code></pre></figure>\n<p><code>drop()</code> takes a value and destroys it.</p>\n<p><em>Takes</em> is the important word. In the borrowing article, passing a <code>Player</code> into a function <strong>moved</strong> it: the function owned it now, and the old variable was empty. <code>drop(game)</code> moves our game into <code>drop</code>, and <code>drop</code> simply ends it.</p>\n<p>Try to use the old game anyway. Put this on the line right after <code>drop(game);</code>:</p>\n<pre><code class=\"language-rust\">    println!(\"{}\", game.stage);\n</code></pre>\n<p>Rust stops you:</p>\n<pre><code class=\"language-text\">error[E0382]: borrow of moved value: `game`\n  --&gt; src\\main.rs:49:20\n   |\n 4 |     let mut game = Game::new();\n   |         -------- move occurs because `game` has type `Game`, which does not implement the `Copy` trait\n...\n48 |     drop(game);\n   |          ---- value moved here\n49 |     println!(\"{}\", game.stage);\n   |                    ^^^^^^^^^^ value borrowed here after move\n</code></pre>\n<p>Delete that line again. <em>Value moved here.</em> After <code>drop(game)</code>, the only things left of the game are the text in <code>save</code> and whatever we rebuild from it.</p>\n<p>So when the program prints <code>Loaded. Stage 4, level 2.</code> and then spends an hour, every number it shows came through that line of JSON.</p>\n<p>Try it on a smaller game. Run it, and you should see <code>Restored game: stage 4</code>. Then remove the <code>//</code> in front of the <code>println!</code> that reads the old game and run it again. Put it back when you have read the error.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · use it after it is gone</figcaption><pre><code class=\"language-rust\">struct Game {\n    stage: u32,\n}\n\nfn main() {\n    let game = Game { stage: 4 };\n    let saved_stage = game.stage;\n\n    drop(game);\n\n    // println!(&quot;Old game: stage {}&quot;, game.stage);\n\n    let game = Game { stage: saved_stage };\n\n    println!(&quot;Restored game: stage {}&quot;, game.stage);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/\">the original article</a>.</p></figure>\n<p>Back in your project, run the real game:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nSaved: {\"save_version\":1,\"stage\":4,\"player\":{\"gold\":11,\"xp\":4,\"level\":2,\"click_upgrade_level\":1},\"enemy_health\":50}\nLoaded. Stage 4, level 2.\nOne hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<p>The last line is the same one the game printed last time, with no save involved. The game that spent the hour was not the game that cleared the stages. It only remembered being it.</p>\n<hr />\n<h2 id=\"a-save-from-the-future\">A save from the future<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#a-save-from-the-future\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now that <code>save_version</code> field.</p>\n<p>Some day the save format will change. Maybe a second upgrade, maybe a pet. When it does, we bump <code>SAVE_VERSION</code> to 2.</p>\n<p>Then someone opens the new version of the game with a save from version 1. Or, worse, opens an old copy of the game with a save from version 2. Their save might even read correctly as JSON, and still mean something different from what this code thinks it means.</p>\n<p>We cannot know today how to convert between formats that do not exist yet. What we can do today is notice the mismatch and refuse, instead of guessing.</p>\n<p>That is a second way loading can fail. And unlike <code>Unreadable</code>, this one has a detail worth keeping: <em>which</em> version the save claimed to be.</p>\n<p><strong>Coding challenge: Refuse a save from the future</strong></p>\n<p>Below is a smaller loader that only reads a version and a stage. It already turns unreadable text into <code>LoadError::Unreadable</code>. Where the <code>// TODO</code> is, add the check that refuses any <code>save_version</code> that is not <code>SAVE_VERSION</code>, with <code>LoadError::UnsupportedVersion</code> holding the version the save claimed.</p>\n<p>Keep the names <code>load_stage</code>, <code>LoadError</code>, <code>Unreadable</code>, <code>UnsupportedVersion</code>, and <code>SAVE_VERSION</code>, because the checks use them. <strong>Check my code</strong> runs hidden tests against your version.</p>\n<p><strong>Expected result:</strong> the starter prints <code>Ok(4)</code>, <code>Ok(4)</code>, <code>Err(Unreadable)</code>. With your check, the middle line becomes <code>Err(UnsupportedVersion(99))</code>. Until then you will also see two \"never used\" warnings, for <code>SAVE_VERSION</code> and <code>UnsupportedVersion</code>. They are a hint: your check is what uses them.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Refuse a save from the future</figcaption><pre><code class=\"language-rust\">use serde::Deserialize;\n\nconst SAVE_VERSION: u32 = 1;\n\n#[derive(Debug, PartialEq)]\nenum LoadError {\n    Unreadable,\n    UnsupportedVersion(u32),\n}\n\n#[derive(Deserialize)]\nstruct SaveData {\n    save_version: u32,\n    stage: u32,\n}\n\nfn load_stage(json: &amp;str) -&gt; Result&lt;u32, LoadError&gt; {\n    let save: SaveData = match serde_json::from_str(json) {\n        Ok(save) =&gt; save,\n        Err(_) =&gt; return Err(LoadError::Unreadable),\n    };\n\n    // TODO: refuse any save_version that is not SAVE_VERSION\n\n    Ok(save.stage)\n}\n\nfn main() {\n    println!(&quot;{:?}&quot;, load_stage(r#&quot;{&quot;save_version&quot;:1,&quot;stage&quot;:4}&quot;#));\n    println!(&quot;{:?}&quot;, load_stage(r#&quot;{&quot;save_version&quot;:99,&quot;stage&quot;:4}&quot;#));\n    println!(&quot;{:?}&quot;, load_stage(&quot;not a save&quot;));\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// A save with the current version should still load and give back its stage.\n#[test]\nfn the_current_version_loads() {\n    assert_eq!(load_stage(r#&quot;{&quot;save_version&quot;:1,&quot;stage&quot;:4}&quot;#), Ok(4));\n}\n\n/// A save from version 99 should be refused with `LoadError::UnsupportedVersion`, carrying the 99 so the caller knows which version it was.\n#[test]\nfn another_version_is_refused() {\n    assert_eq!(\n        load_stage(r#&quot;{&quot;save_version&quot;:99,&quot;stage&quot;:4}&quot;#),\n        Err(LoadError::UnsupportedVersion(99))\n    );\n}\n\n/// Text that is not a save at all should still be `LoadError::Unreadable`. Check the version only after the text has been read.\n#[test]\nfn nonsense_is_still_unreadable() {\n    assert_eq!(load_stage(&quot;not a save&quot;), Err(LoadError::Unreadable));\n}\n\n/// `main()` should run to the end without panicking.\n#[test]\nfn main_runs() {\n    main();\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>It is an <code>if</code> with an early <code>return</code>, the same shape as the \"not enough gold\" check in <code>buy_click_upgrade()</code>: compare <code>save.save_version</code> with <code>SAVE_VERSION</code>, and return <code>Err(LoadError::UnsupportedVersion(save.save_version))</code> when they differ.</p>\n</aside>\n<p>Here is the same change in the real game. First the new variant:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\" data-highlight-lines=\"4\">#[derive(Debug, PartialEq)]\npub enum LoadError {\n    Unreadable,\n    UnsupportedVersion(u32),\n}\n</code></pre></figure>\n<p><code>UnsupportedVersion(u32)</code> is an enum variant that <strong>carries a value</strong>. <code>PurchaseOutcome::Purchased</code> was just a name. <code>LoadError::UnsupportedVersion(99)</code> is a name <em>and</em> a number: the version that got refused. A <code>match</code> can pull that number back out, the same way <code>Ok(save)</code> pulled out the save.</p>\n<p>Then the check, in <code>from_saved_json()</code>, right after the <code>match</code> and before the game is rebuilt:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">        if save.save_version != SAVE_VERSION {\n            return Err(LoadError::UnsupportedVersion(save.save_version));\n        }\n</code></pre></figure>\n<p>There is nothing to migrate yet. Version 1 is the only version that has ever existed. But every save written from today on says which version wrote it, and that is the part we could never add later. A save without a version number stays without one forever.</p>\n<hr />\n<h2 id=\"tests-that-say-no\">Tests that say no<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#tests-that-say-no\" aria-label=\"Link to this section\">#</a></h2>\n<p>A broken save should get a polite no, not a wiped game. These tests prove it does.</p>\n<p>The loader has three ways out: a game, <code>Unreadable</code>, and <code>UnsupportedVersion</code>. Each one deserves a test.</p>\n<p>The obvious way to write the first one does not compile:</p>\n<pre><code class=\"language-rust\">assert_eq!(\n    Game::from_saved_json(\"definitely not a save\"),\n    Err(LoadError::Unreadable)\n);\n</code></pre>\n<pre><code class=\"language-text\">error[E0369]: binary operation `==` cannot be applied to type `Result&lt;game::Game, save::LoadError&gt;`\nnote: an implementation of `PartialEq` might be missing for `game::Game`\n\nerror[E0277]: `game::Game` doesn't implement `Debug`\n</code></pre>\n<p><code>assert_eq!</code> compares the two sides with <code>==</code>, and prints both if they differ. A <code>Result&lt;Game, LoadError&gt;</code> can only do that if <code>Game</code> can, and <code>Game</code> has never needed to be compared or printed. We could derive <code>PartialEq</code> and <code>Debug</code> on <code>Game</code>, <code>Player</code>, and <code>Enemy</code> just for this. But we do not want to compare whole games here. We want to ask one question: <em>is this an <code>Err(LoadError::Unreadable)</code>?</em></p>\n<p>That is what <code>matches!</code> does. The <code>!</code> means it is a macro, like <code>println!</code> and <code>assert_eq!</code>. It takes a value and a pattern, the same kind of pattern a <code>match</code> arm uses, and gives back <code>true</code> if the value fits:</p>\n<pre><code class=\"language-rust\">assert!(matches!(\n    Game::from_saved_json(\"definitely not a save\"),\n    Err(LoadError::Unreadable)\n));\n</code></pre>\n<p>Here are all the new unit tests. They go inside the <code>tests</code> module at the bottom of <code>src/save.rs</code>, after <code>a_new_game_saves_its_starting_numbers</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">    #[test]\n    fn a_loaded_game_rebuilds_the_enemy_from_the_rules() {\n        let mut game = Game::new();\n        game.tick(600);\n\n        let restored = Game::from_saved_json(&amp;game.save_json()).expect(\"the save should load\");\n\n        assert_eq!(restored.stage, game.stage);\n        assert_eq!(restored.enemy.health, game.enemy.health);\n        assert_eq!(restored.enemy.gold_reward, game.enemy.gold_reward);\n        assert_eq!(restored.enemy.xp_reward, game.enemy.xp_reward);\n    }\n\n    #[test]\n    fn nonsense_is_not_a_save() {\n        assert!(matches!(\n            Game::from_saved_json(\"definitely not a save\"),\n            Err(LoadError::Unreadable)\n        ));\n    }\n\n    #[test]\n    fn a_save_with_missing_pieces_is_not_a_save() {\n        let json = r#\"{\"save_version\":1,\"stage\":4}\"#;\n\n        assert!(matches!(\n            Game::from_saved_json(json),\n            Err(LoadError::Unreadable)\n        ));\n    }\n\n    #[test]\n    fn a_save_from_another_version_is_refused() {\n        let json = r#\"{\n            \"save_version\": 99,\n            \"stage\": 1,\n            \"player\": { \"gold\": 0, \"xp\": 0, \"level\": 1, \"click_upgrade_level\": 0 },\n            \"enemy_health\": 20\n        }\"#;\n\n        assert!(matches!(\n            Game::from_saved_json(json),\n            Err(LoadError::UnsupportedVersion(99))\n        ));\n    }\n</code></pre></figure>\n<p>The first one checks the rewards, which are not in the save at all. It can read <code>gold_reward</code> because it lives inside the library, where <code>pub(crate)</code> fields are visible. Here <code>expect()</code> is the right tool: if a save we just made does not load, that is a bug, and the test should fail loudly.</p>\n<p>The last one shows that JSON does not care about spaces or line breaks. The loader reads that save fine. It refuses it anyway, because of the 99.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why do the failure tests use <code>assert!(matches!(...))</code> instead of <code>assert_eq!</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>matches!</code> runs faster than <code>assert_eq!</code></li>\n<li><code>assert_eq!</code> needs both sides to support <code>==</code> and <code>Debug</code>, and <code>Game</code> supports neither</li>\n<li><code>assert_eq!</code> cannot compare enum variants that carry a value</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>assert_eq!</code> needs both sides to support <code>==</code> and <code>Debug</code>, and <code>Game</code> supports neither</p>\n<p><code>matches!</code> only asks whether a value fits a pattern, so <code>Game</code> never has to be compared or printed.</p>\n</div>\n</div>\n</section>\n<p>One more test, from the outside, the same way <code>tests/idle_hour.rs</code> tests the library. Create <code>tests/save_and_restore.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">tests/save_and_restore.rs</figcaption><pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn a_restored_game_plays_the_same_hour() {\n    let mut original = Game::new();\n    original.tick(600);\n\n    let mut restored = Game::from_saved_json(&amp;original.save_json()).expect(\"the save should load\");\n\n    original.tick(3600);\n    restored.tick(3600);\n\n    assert_eq!(restored.stage, original.stage);\n    assert_eq!(restored.enemy.health, original.enemy.health);\n    assert_eq!(restored.player.gold, original.player.gold);\n    assert_eq!(restored.player.xp, original.player.xp);\n    assert_eq!(restored.player.level, original.player.level);\n}\n</code></pre></figure>\n<p>It is a cousin of <code>one_long_tick_matches_many_short_ones</code>. Two games that should be the same, run side by side, compared at the end. If the save left anything out, an hour is plenty of time for the difference to show.</p>\n<p>Run everything:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo clippy\n</code></pre>\n<p>Twenty-eight tests pass: twenty-six inside the library, and one in each of the two files in <code>tests/</code>. Clippy should be quiet.</p>\n<hr />\n<h2 id=\"the-new-and-changed-files\">The new and changed files<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#the-new-and-changed-files\" aria-label=\"Link to this section\">#</a></h2>\n<p>If anything above went sideways, here is every file that changed. The rest are exactly as they were in \"Our starting point.\"</p>\n<p><code>Cargo.toml</code> (only the <code>[dependencies]</code> section changed):</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">Cargo.toml</figcaption><pre><code class=\"language-toml\">[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n</code></pre></figure>\n<p><code>src/lib.rs</code>:</p>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/lib.rs</figcaption><pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use save::LoadError;\n</code></pre></figure>\n<details>\n<summary><code>src/save.rs</code> · 130 lines</summary>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/save.rs</figcaption><pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::game::Game;\nuse serde::{Deserialize, Serialize};\n\nconst SAVE_VERSION: u32 = 1;\n\n#[derive(Debug, PartialEq)]\npub enum LoadError {\n    Unreadable,\n    UnsupportedVersion(u32),\n}\n\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\n#[derive(Serialize, Deserialize)]\nstruct SaveData {\n    save_version: u32,\n    stage: u32,\n    player: PlayerSave,\n    enemy_health: u32,\n}\n\nimpl Game {\n    pub fn save_json(&amp;self) -&gt; String {\n        let save = SaveData {\n            save_version: SAVE_VERSION,\n            stage: self.stage,\n            player: PlayerSave {\n                gold: self.player.gold,\n                xp: self.player.xp,\n                level: self.player.level,\n                click_upgrade_level: self.player.click_upgrade_level,\n            },\n            enemy_health: self.enemy.health,\n        };\n\n        serde_json::to_string(&amp;save).expect(\"a save made of numbers should always serialize\")\n    }\n\n    pub fn from_saved_json(json: &amp;str) -&gt; Result&lt;Game, LoadError&gt; {\n        let save: SaveData = match serde_json::from_str(json) {\n            Ok(save) =&gt; save,\n            Err(_) =&gt; return Err(LoadError::Unreadable),\n        };\n\n        if save.save_version != SAVE_VERSION {\n            return Err(LoadError::UnsupportedVersion(save.save_version));\n        }\n\n        let mut game = Game::new();\n\n        game.stage = save.stage;\n        game.player.gold = save.player.gold;\n        game.player.xp = save.player.xp;\n        game.player.level = save.player.level;\n        game.player.click_upgrade_level = save.player.click_upgrade_level;\n\n        game.enemy = Enemy::for_stage(save.stage);\n        game.enemy.health = save.enemy_health;\n\n        Ok(game)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn a_new_game_saves_its_starting_numbers() {\n        let json = Game::new().save_json();\n\n        assert_eq!(\n            json,\n            r#\"{\"save_version\":1,\"stage\":1,\"player\":{\"gold\":0,\"xp\":0,\"level\":1,\"click_upgrade_level\":0},\"enemy_health\":20}\"#\n        );\n    }\n\n    #[test]\n    fn a_loaded_game_rebuilds_the_enemy_from_the_rules() {\n        let mut game = Game::new();\n        game.tick(600);\n\n        let restored = Game::from_saved_json(&amp;game.save_json()).expect(\"the save should load\");\n\n        assert_eq!(restored.stage, game.stage);\n        assert_eq!(restored.enemy.health, game.enemy.health);\n        assert_eq!(restored.enemy.gold_reward, game.enemy.gold_reward);\n        assert_eq!(restored.enemy.xp_reward, game.enemy.xp_reward);\n    }\n\n    #[test]\n    fn nonsense_is_not_a_save() {\n        assert!(matches!(\n            Game::from_saved_json(\"definitely not a save\"),\n            Err(LoadError::Unreadable)\n        ));\n    }\n\n    #[test]\n    fn a_save_with_missing_pieces_is_not_a_save() {\n        let json = r#\"{\"save_version\":1,\"stage\":4}\"#;\n\n        assert!(matches!(\n            Game::from_saved_json(json),\n            Err(LoadError::Unreadable)\n        ));\n    }\n\n    #[test]\n    fn a_save_from_another_version_is_refused() {\n        let json = r#\"{\n            \"save_version\": 99,\n            \"stage\": 1,\n            \"player\": { \"gold\": 0, \"xp\": 0, \"level\": 1, \"click_upgrade_level\": 0 },\n            \"enemy_health\": 20\n        }\"#;\n\n        assert!(matches!(\n            Game::from_saved_json(json),\n            Err(LoadError::UnsupportedVersion(99))\n        ));\n    }\n}\n</code></pre></figure>\n</details>\n<details>\n<summary><code>src/main.rs</code> · 71 lines</summary>\n<figure class=\"ob-code-block\"><figcaption class=\"ob-code-title\">src/main.rs</figcaption><pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                game.player.xp_needed()\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    game.player.click_damage()\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                game.player.click_damage(),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let save = game.save_json();\n    println!(\"Saved: {save}\");\n\n    drop(game);\n\n    let mut game = match Game::from_saved_json(&amp;save) {\n        Ok(game) =&gt; game,\n        Err(error) =&gt; {\n            println!(\"Could not load the save: {error:?}\");\n            return;\n        }\n    };\n\n    println!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n</code></pre></figure>\n</details>\n<p><code>tests/save_and_restore.rs</code> is exactly as written in the section above.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Two ideas are worth keeping.</p>\n<p>The first is that failure is part of the answer. <code>from_saved_json()</code> does not return a <code>Game</code>. It returns a <code>Result</code>, and anyone who calls it has to deal with both halves before they get a <code>Game</code> out. There is no way to forget. That is <code>Result</code> doing for errors what <code>PurchaseOutcome</code> did for purchases: making the outcome a value with a name, so that <code>false</code> is never the whole story.</p>\n<p>The second is what the save does not contain. No damage numbers, no thresholds, no costs, and no enemy rewards. It stores the player's progress, the stage, and the enemy's remaining health. Everything else is rebuilt from the current rules.</p>\n<p>Along the way, we also encountered:</p>\n<ul>\n<li>dependencies in <code>Cargo.toml</code>, and <code>Cargo.lock</code></li>\n<li><code>serde</code> and <code>serde_json</code></li>\n<li>JSON</li>\n<li>traits, and deriving <code>Serialize</code> and <code>Deserialize</code></li>\n<li><code>const</code></li>\n<li>a save shape kept separate from the runtime <code>Game</code></li>\n<li><code>Result</code>, <code>Ok</code>, and <code>Err</code></li>\n<li><code>match</code> on a <code>Result</code>, and <code>expect()</code> when failure would be a bug</li>\n<li><code>&amp;str</code>, and raw strings</li>\n<li><code>drop()</code>, and the word <em>moved</em></li>\n<li>shadowing: reusing a variable name with a new <code>let</code></li>\n<li>enum variants that carry a value</li>\n<li>a save version, before there is anything to migrate</li>\n<li><code>matches!</code></li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>There is still a loose end. A save is text, and anyone can edit text.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A player opens their save and changes <code>\"gold\":11</code> to <code>\"gold\":11000000</code>. What does <code>from_saved_json()</code> do today?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Returns <code>Err(LoadError::Unreadable)</code>, because the number is too big</li>\n<li>Loads it, because it is still valid JSON with the right version and a number that fits in a <code>u32</code></li>\n<li>Refuses to compile until we add a maximum</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Loads it, because it is still valid JSON with the right version and a number that fits in a <code>u32</code></p>\n<p>The loader checks that the text is a save, not that the save is believable. That is a different job.</p>\n</div>\n</div>\n</section>\n<p>Module privacy keeps other Rust code away from private engine details. It cannot make outside data trustworthy. Checking that a save makes sense is a different boundary, and it gets its own article later in the series. Not the next one.</p>\n<hr />\n<h2 id=\"text-crosses-any-wall\">Text crosses any wall<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#text-crosses-any-wall\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game can now describe itself in text, and rebuild itself from text.</p>\n<p>Text is one of the easiest things for different languages and environments to share. A terminal can print it. A file can hold it. A browser can keep it in storage and hand it back.</p>\n<p>So far, the only thing that has ever driven this engine is <code>main()</code>, in Rust, in a terminal.</p>\n<p>Next time, we compile the library to WebAssembly, a format browsers can run right beside JavaScript, and the thing on the other side of the wall is JavaScript.</p>\n<p>It will not learn how combat works. It will not learn what a slime is worth. It gets a handful of operations and a save that is already text.</p>\n<p>That is all it needs.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-save-load-serde-result/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Closing the Terminal Is Not Game Over.\nCanonical: https://mnixblog.netlify.app/rust-save-load-serde-result/\nSave and Load Your Rust Game With Serde and Result\nOur game can simulate an hour without making us wait an hour.\nIt cannot survive the program ending.\nClose the terminal and everything is gone: the stages, the gold, the levels, the upgrade you bought. The next cargo run starts at stage 1 with a fresh slime, as if nothing ever happened.\nAn idle game that forgets you the moment you leave is not much of an idle game.\nI learned what lost progress feels like on a 386, building DOOM maps. It crashed in the middle of one and took an entire afternoon of carefully placed monsters and secret rooms with it. Nothing has taught me to save my work quite like that afternoon.\nOur game deserves better than my DOOM maps got.\nToday the game learns to write itself down, and to read itself back.\nNot to a file, or a browser, or a server. The library will not know where saves go, and it should not. It will only know how to say, in text:\nThis is everything about me.\nAnd, handed that same text later:\nI remember.\nBy the end, main() clears three stages, saves, throws the game away, loads the save, and then spends the hour away:\nSaved: {\"save_version\":1,\"stage\":4,\"player\":{\"gold\":11,\"xp\":4,\"level\":2,\"click_upgrade_level\":1},\"enemy_health\":50}\nLoaded. Stage 4, level 2.\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nSame hour. Same numbers. A different game object, rebuilt from a line of text.\nSaving always works. Loading might not: the text could be anything. So Rust hands the loaded game back in a box that holds one of two things, the game or the reason there is no game, and will not let us reach for the game until we have said what happens if it is the other one.\nAlong the way we add our first dependency, and that box turns out to be how Rust handles everything that can fail. There are five quick knowledge checks, two live examples you can edit and run in the page, and a coding challenge.\nOur starting point\nThis series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.\nIf you are only reading along, skip ahead to \"A save is just text.\" The code is folded away below, one file at a time.\nIf you are building along, you need the library we built last time: a project called rustbound-idle with six source files and one outside test. (If you kept your project from the last article, you already have all of this. The article_9 branch of the companion repository has it too.)\nrustbound-idle/\n├── Cargo.toml\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   └── game.rs\n└── tests/\n└── idle_hour.rs\nCargo.toml is whatever cargo new rustbound-idle gave you. It should say edition = \"2024\", which needs Rust 1.85 or newer.\n<details>\n<summary><code>src/lib.rs</code> · 8 lines</summary>\nmod enemy;\nmod game;\nmod player;\nmod rules;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</details>\n<details>\n<summary><code>src/main.rs</code> · 56 lines</summary>\nuse rustbound_idle::{Game, PurchaseOutcome};\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\ngame.player.xp_needed()\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\ngame.player.click_damage()\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\ngame.player.click_damage(),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n</details>\n<details>\n<summary><code>src/rules.rs</code> · 105 lines</summary>\npub(crate) fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\npub(crate) fn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\npub(crate) fn is_defeated(health: u32) -> bool {\nhealth == 0\n}\npub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\npub(crate) fn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\npub(crate) fn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\npub(crate) fn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\npub(crate) fn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\npub(crate) fn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\npub(crate) fn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\npub(crate) fn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n}\n</details>\n<details>\n<summary><code>src/enemy.rs</code> · 19 lines</summary>\nuse crate::rules::{enemy_health_for_stage, gold_reward_for_stage, xp_reward_for_stage};\npub struct Enemy {\npub name: String,\npub health: u32,\npub(crate) gold_reward: u32,\npub(crate) xp_reward: u32,\n}\nimpl Enemy {\npub(crate) fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\n</details>\n<details>\n<summary><code>src/player.rs</code> · 114 lines</summary>\nuse crate::rules::{\nbase_damage_for_level, click_damage, click_upgrade_bonus, click_upgrade_cost, xp_to_next_level,\n};\npub struct Player {\npub gold: u32,\npub xp: u32,\npub level: u32,\npub click_upgrade_level: u32,\n}\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\npub(crate) fn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\npub(crate) fn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\npub(crate) fn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\n}\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(self.base_damage(), self.bonus_damage())\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\npub(crate) fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n}\n</details>\n<details>\n<summary><code>src/game.rs</code> · 158 lines</summary>\nuse crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{health_after_hit, idle_damage_for_level, is_defeated};\npub struct Game {\npub stage: u32,\npub player: Player,\npub enemy: Enemy,\n}\nimpl Game {\npub fn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\npub fn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\npub fn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\npub fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nimpl Default for Game {\nfn default() -> Game {\nGame::new()\n}\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nenemy.health = health_after_hit(enemy.health, player.click_damage());\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\n}\n</details>\n<details>\n<summary><code>tests/idle_hour.rs</code> · 11 lines</summary>\nuse rustbound_idle::Game;\n#[test]\nfn an_hour_away_moves_the_game_forward() {\nlet mut game = Game::new();\ngame.tick(3600);\nassert_eq!(game.stage, 128);\nassert_eq!(game.player.level, 47);\n}\n</details>\nRun cargo test, and twenty-two tests pass: twenty-one inside the library and one in tests/idle_hour.rs. Run cargo run, and the player clears three stages, buys an upgrade, and spends an hour away.\nHold on to twenty-two. Today we add six.\nA save is just text\nWhat is a save, exactly?\nStrip away the menus and the little floppy-disk icon, and a save is a description of the game, written down somewhere that outlives the program.\nThat icon was a real object once. Our house had hundreds of them, the 5.25-inch kind that actually flopped, full of games and programs. They are probably still boxed up somewhere at my parents' house. The idea outlived them.\nFor our game, \"the game\" is one Game value: a stage, a player, and the slime they are fighting. Write those numbers down and you have a save. Read them back into a Game and you have loaded it.\nSo the question for today is: write them down how?\nWe could invent a format. One number per line, in a fixed order, and a parser that reads them back:\n4\n11\n4\n2\n1\n50\nIt would work, until the day we add a field in the middle and every old save quietly reads the wrong number into the wrong slot. Nothing crashes. The player just loads in with fifty gold and an upgrade level of eleven.\nThere is a better answer, and most programs that save anything already use it.\nJSON is a plain-text format for describing values with names. Here is a brand-new game, written as JSON:\n{\n\"save_version\": 1,\n\"stage\": 1,\n\"player\": {\n\"gold\": 0,\n\"xp\": 0,\n\"level\": 1,\n\"click_upgrade_level\": 0\n},\n\"enemy_health\": 20\n}\nEvery value has a name, and values can nest: player is a group of four values inside the bigger one.\nNotice that this is not a copy of every field in Game. There is no enemy name and no reward. It is the minimum we need to rebuild the same game: the stage, the player's progress, and how much health the current slime has left.\nThat last one matters more than it looks. Growing up, my brothers and I often played Super Nintendo games on an emulator instead of the console, and a big reason was being able to save anywhere. enemy_health is our version of that. We can save in the middle of a fight, and the slime will still be wounded when we come back.\nThe save_version line looks like it does nothing yet. It is there for a future version of us, and we will come back to it.\nQuestion: Why save the game as named JSON values instead of one number per line?\nJSON files are always smaller\nEvery value carries its name, so a new or reordered field cannot slide into the wrong slot\nRust cannot read a file of plain numbers\nAnswer: 2. Every value carries its name, so a new or reordered field cannot slide into the wrong slot\nExplanation: A fixed order is a promise that nothing will ever change. Names let the loader find gold wherever it is.\nWe are not going to write the JSON writer and reader by hand. Someone already has, and it is very good.\nThe first dependency\nUntil now, every line of this game has been ours. Today we borrow some.\nRust code that other people share is published as crates, and most of them live on crates.io (https://crates.io). A crate your project uses is called a dependency. We are adding two:\nserde knows how to take a Rust value apart into named pieces, and put it back together. The name is short for serialize and deserialize, which are the formal words for writing a value down and reading it back.\nserde_json knows how to write those pieces as JSON, and read JSON back into pieces.\nOpen Cargo.toml. The [dependencies] section has been empty since the first article. Fill it in:\nCargo.toml\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n\"1\" means any version 1.x. Cargo picks the newest 1.x it can find, and never jumps to a 2.0 on its own.\nThe features = [\"derive\"] part matters, and we will see why in a moment.\nBuild it:\ncargo build\nThe first time, Cargo downloads the two crates and the handful of smaller ones they depend on, and compiles all of them. You will see a dozen Compiling lines you did not write, and then your own:\nCompiling serde v1.0.229\nCompiling serde_json v1.0.154\nCompiling serde_derive v1.0.229\n...\nCompiling rustbound-idle v0.1.0\nYour version numbers may be newer. That is fine.\nThat only happens once. Cargo keeps the compiled crates around, and later builds are as fast as before.\nLook in your project folder and you will find a new file, Cargo.lock. Cargo wrote it. It records the exact versions that got picked, so that anyone who builds this project later gets the same ones. Leave it alone, and if you use Git, commit it.\nGive the save its own shape\nserde can turn Rust values into text and back again, but there is a choice to make first.\nWe could teach serde about Game, Player, and Enemy directly.\nThat would work.\nIt would also make our runtime structs the permanent shape of our save files.\nThose are not the same job.\nA Game is whatever the engine needs while it is running.\nA save is the minimum information needed to rebuild that game later.\nThe difference matters because some values can be recalculated from the rules. An enemy's gold and XP rewards come from its stage. If we saved those numbers, a save made today would carry today's rewards into next month's version of the game, even after we rebalanced them.\nSo the save gets its own small types.\nCreate src/save.rs:\nsrc/save.rs\nuse serde::{Deserialize, Serialize};\nconst SAVE_VERSION: u32 = 1;\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\n#[derive(Serialize, Deserialize)]\nstruct SaveData {\nsave_version: u32,\nstage: u32,\nplayer: PlayerSave,\nenemy_health: u32,\n}\nThe two structs are the JSON from before, written as Rust. Same names, same nesting.\nconst declares a constant: a value fixed when the program is compiled, which can never change while it runs. Constants are written in capitals by convention, and they need their type spelled out. SAVE_VERSION is the version of the save format this code writes.\nThe interesting line is the one above each struct.\nAsking for a trait\nIn plain terms, that line tells Rust: these two structs can be written down, and read back.\n#[derive(Serialize, Deserialize)] is the same kind of line as the #[derive(Debug, PartialEq)] we put on PurchaseOutcome.\nSerialize and Deserialize are traits. A trait is a named ability a type can have. PartialEq is the ability to be compared with ==. Serialize is the ability to be taken apart into named pieces. Deserialize is the ability to be built back up from them.\nderive asks the compiler to write that ability for us, by looking at the fields. We could write it by hand. Nobody wants to. The thing that does the writing is called a derive macro; a macro is code that writes code.\nThat is also what features = [\"derive\"] was for. Without it, serde gives us the traits but not the code that derives them, and the build stops on every struct:\nerror: cannot find derive macro `Serialize` in this scope\n--> src\\save.rs:5:10\n|\n5 | #[derive(Serialize, Deserialize)]\n|          ^^^^^^^^^\n|\nnote: `Serialize` is imported here, but it is only a trait, without a derive macro\nOnly a trait, without a derive macro. The ability exists; the machine that writes it for us was left out.\nLast, the library has to know the new file exists. Add mod save; to src/lib.rs:\nsrc/lib.rs\nmod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\nIf you build now, Cargo warns that SAVE_VERSION, PlayerSave, and SaveData are never used. Fair. The next section uses them.\nQuestion: Why does the save store the stage but not the enemy's gold_reward?\ngold_reward is pub(crate), so serde cannot see it\nJSON cannot hold more than six numbers\nThe reward can be rebuilt from the stage, so an old save cannot carry old balance rules into a newer game\nAnswer: 3. The reward can be rebuilt from the stage, so an old save cannot carry old balance rules into a newer game\nExplanation: Save what only the player could have done. Rebuild everything else from the current rules.\nWriting it down\nSaving is the easy direction. Every Game can be described, so save_json() always gets an answer.\nAdd an impl Game block to src/save.rs, below the structs. It is a second impl block for Game, in a different file from the first, and Rust is fine with that: an impl block can live anywhere in the crate that owns the type.\nFirst, one more import at the top of the file, beside the serde one:\nsrc/save.rs\nuse crate::game::Game;\nThen the block itself:\nsrc/save.rs\nimpl Game {\npub fn save_json(&self) -> String {\nlet save = SaveData {\nsave_version: SAVE_VERSION,\nstage: self.stage,\nplayer: PlayerSave {\ngold: self.player.gold,\nxp: self.player.xp,\nlevel: self.player.level,\nclick_upgrade_level: self.player.click_upgrade_level,\n},\nenemy_health: self.enemy.health,\n};\nserde_json::to_string(&save).expect(\"a save made of numbers should always serialize\")\n}\n}\nThe method copies the numbers that matter out of the running game into a SaveData, and hands it to serde_json::to_string.\nThat last line has a word we have not seen.\nserde_json::to_string does not hand back a String. It hands back a Result: either Ok, holding the text, or Err, holding what went wrong. It has to, because some Rust values cannot be written as JSON at all.\nOurs can. A struct full of numbers always can. So we call .expect(), which says:\nGive me what is inside the Ok. If it is somehow an Err, stop the program with this message.\nexpect() is for the cases where failure would mean a bug in our own code, not bad luck. We will meet a case that is pure bad luck in a moment, and there we will not use it.\nNow a test that pins the format down. Add it to the bottom of src/save.rs:\nsrc/save.rs\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn a_new_game_saves_its_starting_numbers() {\nlet json = Game::new().save_json();\nassert_eq!(\njson,\nr#\"{\"save_version\":1,\"stage\":1,\"player\":{\"gold\":0,\"xp\":0,\"level\":1,\"click_upgrade_level\":0},\"enemy_health\":20}\"#\n);\n}\n}\nThat string starts with r#\" and ends with \"#. It is a raw string. JSON is full of double quotes, and in a normal string every one of them would need a backslash in front of it. In a raw string, a \" is just a character, and only \"# ends it.\nserde_json::to_string writes everything on one line with no spaces, which is why the expected text is one long line.\ncargo test\nTwenty-three tests pass.\nReading it back\nLoading is the hard direction.\nSaving starts from a Game, which is always valid. Loading starts from text, and text can be anything. A save from an older version. A half-written file. Somebody's grocery list.\nSo what should a load function return when the text is not a save?\nA Game? Which one? A brand-new game would quietly wipe out the player's progress, and nothing would tell them why. It is the same trap as the shop that answered false in an earlier lesson: an answer that looks like an answer, where a wrong guess about what it means looks just like success.\nThe honest return type is the one serde_json already used on us:\nResult<Game, LoadError>\nEither a Game, or the reason there is no Game.\nLoadError is ours to define. It is an enum, like PurchaseOutcome, listing the ways loading can fail. For now there is one. Add it near the top of src/save.rs, below SAVE_VERSION:\nsrc/save.rs\n#[derive(Debug, PartialEq)]\npub enum LoadError {\nUnreadable,\n}\nThen the load itself, inside the same impl Game block, below save_json():\nsrc/save.rs\npub fn from_saved_json(json: &str) -> Result<Game, LoadError> {\nlet save: SaveData = match serde_json::from_str(json) {\nOk(save) => save,\nErr(_) => return Err(LoadError::Unreadable),\n};\nlet mut game = Game::new();\ngame.stage = save.stage;\ngame.player.gold = save.player.gold;\ngame.player.xp = save.player.xp;\ngame.player.level = save.player.level;\ngame.player.click_upgrade_level = save.player.click_upgrade_level;\ngame.enemy = Enemy::for_stage(save.stage);\ngame.enemy.health = save.enemy_health;\nOk(game)\n}\nAnd one more import at the top, because we build an enemy:\nsrc/save.rs\nuse crate::enemy::Enemy;\nLet us walk through it.\njson: &str means the function borrows some text instead of taking it. &str is a borrowed piece of text, the way &Player is a borrowed player. Whoever calls from_saved_json() keeps their String.\nserde_json::from_str(json) reads the text. It returns a Result too, and this time failure is real: the text might be anything. So we match on it, exactly the way we matched on PurchaseOutcome, one arm per possibility:\nOk(save) => save takes the SaveData out of the Ok and makes it the value of the whole match.\nErr(_) => return Err(LoadError::Unreadable) ignores the details of what went wrong (that is the _) and leaves the function early with our own error.\nThe : SaveData after let save matters more than it looks. from_str can build anything that implements Deserialize. That annotation is how it knows to build a SaveData, and therefore which names to look for.\nThe rest rebuilds the game. Start from a new Game, copy the saved progress over it, then ask Enemy::for_stage() for the enemy. That one line is where the save's small shape pays off. The rewards come from the current rules, not from the file. Only the slime's remaining health comes from the save.\nLast, Ok(game) wraps the finished game in the success half of the Result.\nHere is the same round trip on its own. It runs in the page, using the same two crates.\nRun it, and you should see the player saved and loaded. Then break the save on purpose: delete \"level\":2, from the text line and run again. Then put it back and change 11 to -5.\nLive example · break the save on purpose\nuse serde::{Deserialize, Serialize};\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nfn main() {\nlet player = PlayerSave {\ngold: 11,\nxp: 4,\nlevel: 2,\nclick_upgrade_level: 1,\n};\nlet json = serde_json::to_string(&player)\n.expect(\"a save made of numbers should always serialize\");\nprintln!(\"Saved: {json}\");\nlet text = r#\"{\"gold\":11,\"xp\":4,\"level\":2,\"click_upgrade_level\":1}\"#;\nlet loaded: Result<PlayerSave, serde_json::Error> = serde_json::from_str(text);\nmatch loaded {\nOk(player) => println!(\"Loaded: level {} with {} gold\", player.level, player.gold),\nErr(error) => println!(\"Not a save: {error}\"),\n}\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-save-load-serde-result/).\nThe first break prints:\nNot a save: missing field `level` at line 1 column 42\nThe second:\nNot a save: invalid value: integer `-5`, expected u32 at line 1 column 10\nserde_json knew exactly what was wrong both times. from_saved_json() throws those details away and says Unreadable, because the game cannot do anything more useful with them. Either way, the Err arm prints and nothing crashes.\nQuestion: Why does from_saved_json() return Result<Game, LoadError> instead of a Game?\nA Result loads faster than a plain struct\nText that is not a save has no honest Game to return, and the caller has to handle both cases\nserde_json only works inside functions that return a Result\nAnswer: 2. Text that is not a save has no honest Game to return, and the caller has to handle both cases\nExplanation: Returning Game::new() on failure would look like success and quietly wipe the player's progress.\nRust will not let us forget\nTime to use it. In src/main.rs, after the while loop and before the hour away, save the game:\nsrc/main.rs\nlet save = game.save_json();\nprintln!(\"Saved: {save}\");\nThen load it back and print where we are. The tempting version is two lines, right under the save lines:\nlet mut game = Game::from_saved_json(&save);\nprintln!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\nBuild that, and Rust refuses, several times over:\nerror[E0609]: no field `stage` on type `Result<Game, rustbound_idle::save::LoadError>`\n--> src\\main.rs:49:50\n|\n49 |     println!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\n|                                                  ^^^^^ unknown field\nThe other errors say the same thing about player, and about every later line that touches game.\ngame is not a Game. It is a Result, and a Result has no stage. The Game is inside it, maybe.\nThat is the whole point. Other languages often hand back the game or a special empty value and trust you to check. Rust hands back a box with one of two things in it, and will not let you reach for the game until you have said what happens if it is the other thing.\nSo we say it, with a match. Replace the two tempting lines with:\nsrc/main.rs\nlet mut game = match Game::from_saved_json(&save) {\nOk(game) => game,\nErr(error) => {\nprintln!(\"Could not load the save: {error:?}\");\nreturn;\n}\n};\nprintln!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\nIf the load worked, the Game comes out of the Ok. If not, we print the error and return from main(), which ends the program. {error:?} prints the error with Debug, which is why LoadError derives it.\nAnyone outside the library who wants to match on the different kinds of LoadError has to be able to name it. So it goes on the front door, in src/lib.rs:\nsrc/lib.rs\npub use save::LoadError;\nmain.rs does not name it yet. The browser version of the game, a couple of lessons from now, will.\nThrow the game away\nLook at the code we just wrote again. There is something odd about it.\nWe already had a game. Then we wrote let mut game = ... a second time.\nThat is allowed. A new let with an old name makes a brand-new variable, and the old one can no longer be reached by that name. Rust calls this shadowing.\nBut the old game is still there, sitting in memory until main() ends. And we want to prove the load works, not just print a game that might secretly be the old one.\nSo we throw the old one away first, on purpose, right after saving:\nsrc/main.rs\nlet save = game.save_json();\nprintln!(\"Saved: {save}\");\ndrop(game);\ndrop() takes a value and destroys it.\nTakes is the important word. In the borrowing article, passing a Player into a function moved it: the function owned it now, and the old variable was empty. drop(game) moves our game into drop, and drop simply ends it.\nTry to use the old game anyway. Put this on the line right after drop(game);:\nprintln!(\"{}\", game.stage);\nRust stops you:\nerror[E0382]: borrow of moved value: `game`\n--> src\\main.rs:49:20\n|\n4 |     let mut game = Game::new();\n|         -------- move occurs because `game` has type `Game`, which does not implement the `Copy` trait\n...\n48 |     drop(game);\n|          ---- value moved here\n49 |     println!(\"{}\", game.stage);\n|                    ^^^^^^^^^^ value borrowed here after move\nDelete that line again. Value moved here. After drop(game), the only things left of the game are the text in save and whatever we rebuild from it.\nSo when the program prints Loaded. Stage 4, level 2. and then spends an hour, every number it shows came through that line of JSON.\nTry it on a smaller game. Run it, and you should see Restored game: stage 4. Then remove the // in front of the println! that reads the old game and run it again. Put it back when you have read the error.\nLive example · use it after it is gone\nstruct Game {\nstage: u32,\n}\nfn main() {\nlet game = Game { stage: 4 };\nlet saved_stage = game.stage;\ndrop(game);\n// println!(\"Old game: stage {}\", game.stage);\nlet game = Game { stage: saved_stage };\nprintln!(\"Restored game: stage {}\", game.stage);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-save-load-serde-result/).\nBack in your project, run the real game:\ncargo run\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nSaved: {\"save_version\":1,\"stage\":4,\"player\":{\"gold\":11,\"xp\":4,\"level\":2,\"click_upgrade_level\":1},\"enemy_health\":50}\nLoaded. Stage 4, level 2.\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nThe last line is the same one the game printed last time, with no save involved. The game that spent the hour was not the game that cleared the stages. It only remembered being it.\nA save from the future\nNow that save_version field.\nSome day the save format will change. Maybe a second upgrade, maybe a pet. When it does, we bump SAVE_VERSION to 2.\nThen someone opens the new version of the game with a save from version 1. Or, worse, opens an old copy of the game with a save from version 2. Their save might even read correctly as JSON, and still mean something different from what this code thinks it means.\nWe cannot know today how to convert between formats that do not exist yet. What we can do today is notice the mismatch and refuse, instead of guessing.\nThat is a second way loading can fail. And unlike Unreadable, this one has a detail worth keeping: which version the save claimed to be.\nCoding challenge: Refuse a save from the future\nBelow is a smaller loader that only reads a version and a stage. It already turns unreadable text into LoadError::Unreadable. Where the // TODO is, add the check that refuses any save_version that is not SAVE_VERSION, with LoadError::UnsupportedVersion holding the version the save claimed.\nKeep the names load_stage, LoadError, Unreadable, UnsupportedVersion, and SAVE_VERSION, because the checks use them. Check my code runs hidden tests against your version.\nExpected result: the starter prints Ok(4), Ok(4), Err(Unreadable). With your check, the middle line becomes Err(UnsupportedVersion(99)). Until then you will also see two \"never used\" warnings, for SAVE_VERSION and UnsupportedVersion. They are a hint: your check is what uses them.\nRefuse a save from the future\nuse serde::Deserialize;\nconst SAVE_VERSION: u32 = 1;\n#[derive(Debug, PartialEq)]\nenum LoadError {\nUnreadable,\nUnsupportedVersion(u32),\n}\n#[derive(Deserialize)]\nstruct SaveData {\nsave_version: u32,\nstage: u32,\n}\nfn load_stage(json: &str) -> Result<u32, LoadError> {\nlet save: SaveData = match serde_json::from_str(json) {\nOk(save) => save,\nErr(_) => return Err(LoadError::Unreadable),\n};\n// TODO: refuse any save_version that is not SAVE_VERSION\nOk(save.stage)\n}\nfn main() {\nprintln!(\"{:?}\", load_stage(r#\"{\"save_version\":1,\"stage\":4}\"#));\nprintln!(\"{:?}\", load_stage(r#\"{\"save_version\":99,\"stage\":4}\"#));\nprintln!(\"{:?}\", load_stage(\"not a save\"));\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// A save with the current version should still load and give back its stage.\n#[test]\nfn the_current_version_loads() {\nassert_eq!(load_stage(r#\"{\"save_version\":1,\"stage\":4}\"#), Ok(4));\n}\n/// A save from version 99 should be refused with `LoadError::UnsupportedVersion`, carrying the 99 so the caller knows which version it was.\n#[test]\nfn another_version_is_refused() {\nassert_eq!(\nload_stage(r#\"{\"save_version\":99,\"stage\":4}\"#),\nErr(LoadError::UnsupportedVersion(99))\n);\n}\n/// Text that is not a save at all should still be `LoadError::Unreadable`. Check the version only after the text has been read.\n#[test]\nfn nonsense_is_still_unreadable() {\nassert_eq!(load_stage(\"not a save\"), Err(LoadError::Unreadable));\n}\n/// `main()` should run to the end without panicking.\n#[test]\nfn main_runs() {\nmain();\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-save-load-serde-result/).\nTip:\nIt is an if with an early return, the same shape as the \"not enough gold\" check in buy_click_upgrade(): compare save.save_version with SAVE_VERSION, and return Err(LoadError::UnsupportedVersion(save.save_version)) when they differ.\nHere is the same change in the real game. First the new variant:\nsrc/save.rs\n#[derive(Debug, PartialEq)]\npub enum LoadError {\nUnreadable,\nUnsupportedVersion(u32),\n}\nUnsupportedVersion(u32) is an enum variant that carries a value. PurchaseOutcome::Purchased was just a name. LoadError::UnsupportedVersion(99) is a name and a number: the version that got refused. A match can pull that number back out, the same way Ok(save) pulled out the save.\nThen the check, in from_saved_json(), right after the match and before the game is rebuilt:\nsrc/save.rs\nif save.save_version != SAVE_VERSION {\nreturn Err(LoadError::UnsupportedVersion(save.save_version));\n}\nThere is nothing to migrate yet. Version 1 is the only version that has ever existed. But every save written from today on says which version wrote it, and that is the part we could never add later. A save without a version number stays without one forever.\nTests that say no\nA broken save should get a polite no, not a wiped game. These tests prove it does.\nThe loader has three ways out: a game, Unreadable, and UnsupportedVersion. Each one deserves a test.\nThe obvious way to write the first one does not compile:\nassert_eq!(\nGame::from_saved_json(\"definitely not a save\"),\nErr(LoadError::Unreadable)\n);\nerror[E0369]: binary operation `==` cannot be applied to type `Result<game::Game, save::LoadError>`\nnote: an implementation of `PartialEq` might be missing for `game::Game`\nerror[E0277]: `game::Game` doesn't implement `Debug`\nassert_eq! compares the two sides with ==, and prints both if they differ. A Result<Game, LoadError> can only do that if Game can, and Game has never needed to be compared or printed. We could derive PartialEq and Debug on Game, Player, and Enemy just for this. But we do not want to compare whole games here. We want to ask one question: is this an Err(LoadError::Unreadable)?\nThat is what matches! does. The ! means it is a macro, like println! and assert_eq!. It takes a value and a pattern, the same kind of pattern a match arm uses, and gives back true if the value fits:\nassert!(matches!(\nGame::from_saved_json(\"definitely not a save\"),\nErr(LoadError::Unreadable)\n));\nHere are all the new unit tests. They go inside the tests module at the bottom of src/save.rs, after a_new_game_saves_its_starting_numbers:\nsrc/save.rs\n#[test]\nfn a_loaded_game_rebuilds_the_enemy_from_the_rules() {\nlet mut game = Game::new();\ngame.tick(600);\nlet restored = Game::from_saved_json(&game.save_json()).expect(\"the save should load\");\nassert_eq!(restored.stage, game.stage);\nassert_eq!(restored.enemy.health, game.enemy.health);\nassert_eq!(restored.enemy.gold_reward, game.enemy.gold_reward);\nassert_eq!(restored.enemy.xp_reward, game.enemy.xp_reward);\n}\n#[test]\nfn nonsense_is_not_a_save() {\nassert!(matches!(\nGame::from_saved_json(\"definitely not a save\"),\nErr(LoadError::Unreadable)\n));\n}\n#[test]\nfn a_save_with_missing_pieces_is_not_a_save() {\nlet json = r#\"{\"save_version\":1,\"stage\":4}\"#;\nassert!(matches!(\nGame::from_saved_json(json),\nErr(LoadError::Unreadable)\n));\n}\n#[test]\nfn a_save_from_another_version_is_refused() {\nlet json = r#\"{\n\"save_version\": 99,\n\"stage\": 1,\n\"player\": { \"gold\": 0, \"xp\": 0, \"level\": 1, \"click_upgrade_level\": 0 },\n\"enemy_health\": 20\n}\"#;\nassert!(matches!(\nGame::from_saved_json(json),\nErr(LoadError::UnsupportedVersion(99))\n));\n}\nThe first one checks the rewards, which are not in the save at all. It can read gold_reward because it lives inside the library, where pub(crate) fields are visible. Here expect() is the right tool: if a save we just made does not load, that is a bug, and the test should fail loudly.\nThe last one shows that JSON does not care about spaces or line breaks. The loader reads that save fine. It refuses it anyway, because of the 99.\nQuestion: Why do the failure tests use assert!(matches!(...)) instead of assert_eq!?\nmatches! runs faster than assert_eq!\nassert_eq! needs both sides to support == and Debug, and Game supports neither\nassert_eq! cannot compare enum variants that carry a value\nAnswer: 2. assert_eq! needs both sides to support == and Debug, and Game supports neither\nExplanation: matches! only asks whether a value fits a pattern, so Game never has to be compared or printed.\nOne more test, from the outside, the same way tests/idle_hour.rs tests the library. Create tests/save_and_restore.rs:\ntests/save_and_restore.rs\nuse rustbound_idle::Game;\n#[test]\nfn a_restored_game_plays_the_same_hour() {\nlet mut original = Game::new();\noriginal.tick(600);\nlet mut restored = Game::from_saved_json(&original.save_json()).expect(\"the save should load\");\noriginal.tick(3600);\nrestored.tick(3600);\nassert_eq!(restored.stage, original.stage);\nassert_eq!(restored.enemy.health, original.enemy.health);\nassert_eq!(restored.player.gold, original.player.gold);\nassert_eq!(restored.player.xp, original.player.xp);\nassert_eq!(restored.player.level, original.player.level);\n}\nIt is a cousin of one_long_tick_matches_many_short_ones. Two games that should be the same, run side by side, compared at the end. If the save left anything out, an hour is plenty of time for the difference to show.\nRun everything:\ncargo test\ncargo clippy\nTwenty-eight tests pass: twenty-six inside the library, and one in each of the two files in tests/. Clippy should be quiet.\nThe new and changed files\nIf anything above went sideways, here is every file that changed. The rest are exactly as they were in \"Our starting point.\"\nCargo.toml (only the [dependencies] section changed):\nCargo.toml\n[package]\nname = \"rustbound-idle\"\nversion = \"0.1.0\"\nedition = \"2024\"\n[dependencies]\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nsrc/lib.rs:\nsrc/lib.rs\nmod enemy;\nmod game;\nmod player;\nmod rules;\nmod save;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use save::LoadError;\n<details>\n<summary><code>src/save.rs</code> · 130 lines</summary>\nsrc/save.rs\nuse crate::enemy::Enemy;\nuse crate::game::Game;\nuse serde::{Deserialize, Serialize};\nconst SAVE_VERSION: u32 = 1;\n#[derive(Debug, PartialEq)]\npub enum LoadError {\nUnreadable,\nUnsupportedVersion(u32),\n}\n#[derive(Serialize, Deserialize)]\nstruct PlayerSave {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\n#[derive(Serialize, Deserialize)]\nstruct SaveData {\nsave_version: u32,\nstage: u32,\nplayer: PlayerSave,\nenemy_health: u32,\n}\nimpl Game {\npub fn save_json(&self) -> String {\nlet save = SaveData {\nsave_version: SAVE_VERSION,\nstage: self.stage,\nplayer: PlayerSave {\ngold: self.player.gold,\nxp: self.player.xp,\nlevel: self.player.level,\nclick_upgrade_level: self.player.click_upgrade_level,\n},\nenemy_health: self.enemy.health,\n};\nserde_json::to_string(&save).expect(\"a save made of numbers should always serialize\")\n}\npub fn from_saved_json(json: &str) -> Result<Game, LoadError> {\nlet save: SaveData = match serde_json::from_str(json) {\nOk(save) => save,\nErr(_) => return Err(LoadError::Unreadable),\n};\nif save.save_version != SAVE_VERSION {\nreturn Err(LoadError::UnsupportedVersion(save.save_version));\n}\nlet mut game = Game::new();\ngame.stage = save.stage;\ngame.player.gold = save.player.gold;\ngame.player.xp = save.player.xp;\ngame.player.level = save.player.level;\ngame.player.click_upgrade_level = save.player.click_upgrade_level;\ngame.enemy = Enemy::for_stage(save.stage);\ngame.enemy.health = save.enemy_health;\nOk(game)\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn a_new_game_saves_its_starting_numbers() {\nlet json = Game::new().save_json();\nassert_eq!(\njson,\nr#\"{\"save_version\":1,\"stage\":1,\"player\":{\"gold\":0,\"xp\":0,\"level\":1,\"click_upgrade_level\":0},\"enemy_health\":20}\"#\n);\n}\n#[test]\nfn a_loaded_game_rebuilds_the_enemy_from_the_rules() {\nlet mut game = Game::new();\ngame.tick(600);\nlet restored = Game::from_saved_json(&game.save_json()).expect(\"the save should load\");\nassert_eq!(restored.stage, game.stage);\nassert_eq!(restored.enemy.health, game.enemy.health);\nassert_eq!(restored.enemy.gold_reward, game.enemy.gold_reward);\nassert_eq!(restored.enemy.xp_reward, game.enemy.xp_reward);\n}\n#[test]\nfn nonsense_is_not_a_save() {\nassert!(matches!(\nGame::from_saved_json(\"definitely not a save\"),\nErr(LoadError::Unreadable)\n));\n}\n#[test]\nfn a_save_with_missing_pieces_is_not_a_save() {\nlet json = r#\"{\"save_version\":1,\"stage\":4}\"#;\nassert!(matches!(\nGame::from_saved_json(json),\nErr(LoadError::Unreadable)\n));\n}\n#[test]\nfn a_save_from_another_version_is_refused() {\nlet json = r#\"{\n\"save_version\": 99,\n\"stage\": 1,\n\"player\": { \"gold\": 0, \"xp\": 0, \"level\": 1, \"click_upgrade_level\": 0 },\n\"enemy_health\": 20\n}\"#;\nassert!(matches!(\nGame::from_saved_json(json),\nErr(LoadError::UnsupportedVersion(99))\n));\n}\n}\n</details>\n<details>\n<summary><code>src/main.rs</code> · 71 lines</summary>\nsrc/main.rs\nuse rustbound_idle::{Game, PurchaseOutcome};\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\ngame.player.xp_needed()\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\ngame.player.click_damage()\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\ngame.player.click_damage(),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet save = game.save_json();\nprintln!(\"Saved: {save}\");\ndrop(game);\nlet mut game = match Game::from_saved_json(&save) {\nOk(game) => game,\nErr(error) => {\nprintln!(\"Could not load the save: {error:?}\");\nreturn;\n}\n};\nprintln!(\"Loaded. Stage {}, level {}.\", game.stage, game.player.level);\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n</details>\ntests/save_and_restore.rs is exactly as written in the section above.\nWhat we learned\nTwo ideas are worth keeping.\nThe first is that failure is part of the answer. from_saved_json() does not return a Game. It returns a Result, and anyone who calls it has to deal with both halves before they get a Game out. There is no way to forget. That is Result doing for errors what PurchaseOutcome did for purchases: making the outcome a value with a name, so that false is never the whole story.\nThe second is what the save does not contain. No damage numbers, no thresholds, no costs, and no enemy rewards. It stores the player's progress, the stage, and the enemy's remaining health. Everything else is rebuilt from the current rules.\nAlong the way, we also encountered:\ndependencies in Cargo.toml, and Cargo.lock\nserde and serde_json\nJSON\ntraits, and deriving Serialize and Deserialize\nconst\na save shape kept separate from the runtime Game\nResult, Ok, and Err\nmatch on a Result, and expect() when failure would be a bug\n&str, and raw strings\ndrop(), and the word moved\nshadowing: reusing a variable name with a new let\nenum variants that carry a value\na save version, before there is anything to migrate\nmatches!\nYou do not need to memorize all of that yet.\nThere is still a loose end. A save is text, and anyone can edit text.\nQuestion: A player opens their save and changes \"gold\":11 to \"gold\":11000000. What does from_saved_json() do today?\nReturns Err(LoadError::Unreadable), because the number is too big\nLoads it, because it is still valid JSON with the right version and a number that fits in a u32\nRefuses to compile until we add a maximum\nAnswer: 2. Loads it, because it is still valid JSON with the right version and a number that fits in a u32\nExplanation: The loader checks that the text is a save, not that the save is believable. That is a different job.\nModule privacy keeps other Rust code away from private engine details. It cannot make outside data trustworthy. Checking that a save makes sense is a different boundary, and it gets its own article later in the series. Not the next one.\nText crosses any wall\nOur game can now describe itself in text, and rebuild itself from text.\nText is one of the easiest things for different languages and environments to share. A terminal can print it. A file can hold it. A browser can keep it in storage and hand it back.\nSo far, the only thing that has ever driven this engine is main(), in Rust, in a terminal.\nNext time, we compile the library to WebAssembly, a format browsers can run right beside JavaScript, and the thing on the other side of the wall is JavaScript.\nIt will not learn how combat works. It will not learn what a slime is worth. It gets a handful of operations and a save that is already text.\nThat is all it needs.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-save-load-serde-result/#ob-discussion)",
      "date_published": "2026-10-27T18:00:00Z",
      "id": "https://mnixblog.netlify.app/rust-save-load-serde-result/",
      "image": "https://mnixblog.netlify.app/images/rust-save-load-serde-result/cover.jpg",
      "summary": "Teach a Rust idle game to save itself as JSON and load itself back. Add your first dependency with serde, and let Result handle a save that will not load.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Closing the Terminal Is Not Game Over.",
      "url": "https://mnixblog.netlify.app/rust-save-load-serde-result/"
    },
    {
      "content_html": "<p>Our game works, but its rules and its screen are tangled together in one file of more than four hundred lines. Today we pull them apart, so the same game can run in a terminal now and in a browser later.</p>\n<p>Nothing about the game changes. We will not touch a rule.</p>\n<p>Nobody planned that file. Each article needed one more thing, and each thing went in the next free spot. But look at what is in there: everything above <code>main()</code> knows nothing about a terminal or a screen. Only <code>main()</code> does.</p>\n<p>So we put a wall between them. When we are done, <code>main.rs</code> is fifty-six lines: it creates a game, clicks, and prints. The game itself lives in a library that has never heard of a terminal.</p>\n<p>A library like that, made of rules and state that do not know how they are being displayed, is the heart of what people call a game engine.</p>\n<p>A small one.</p>\n<p>Ours will have a front door and nothing else.</p>\n<p>The program still prints the same thing, a few stages cleared and then an hour of waiting:</p>\n<pre><code class=\"language-text\">One hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<p>Same output.</p>\n<p>Same twenty-one tests, plus one new one.</p>\n<p>Different house.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>This series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.</p>\n<p>If you are only reading along, skip past this code to \"The groups are already visible.\"</p>\n<p>If you are building along, create a project with <code>cargo new rustbound-idle</code> if you do not already have one. The name matters later.</p>\n<p>At this point, <code>src/main.rs</code> holds three structs, an enum, three <code>impl</code> blocks, twelve small functions, and twenty-one tests:</p>\n<details>\n<summary><code>src/main.rs</code> · 428-line starting point</summary>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::NotEnoughGold\n        );\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::Purchased\n        );\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(long.enemy.health, short.enemy.health);\n        assert_eq!(long.player.gold, short.player.gold);\n        assert_eq!(long.player.xp, short.player.xp);\n        assert_eq!(long.player.level, short.player.level);\n    }\n}\n</code></pre>\n</details>\n<p>Run it, and the player clears three stages, buys an upgrade, and spends an hour away.</p>\n<p>Run <code>cargo test</code>, and all twenty-one tests pass.</p>\n<p>You need a recent Rust version. This project uses edition 2024, which requires Rust 1.85 or newer.</p>\n<p>Hold on to that test count.</p>\n<p>Everything we do today is a reorganization, and the tests are how we prove we did not break anything.</p>\n<hr />\n<h2 id=\"the-groups-are-already-visible\">The groups are already visible<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-groups-are-already-visible\" aria-label=\"Link to this section\">#</a></h2>\n<p>Nobody has to invent categories for this code.</p>\n<p>It has been sorting itself since the first article:</p>\n<pre><code class=\"language-text\">rules    click_damage, health_after_hit, is_defeated,\n         enemy_health_for_stage, gold_reward_for_stage,\n         xp_reward_for_stage, xp_to_next_level,\n         base_damage_for_level, idle_damage_for_level,\n         click_upgrade_bonus, click_upgrade_cost\n\nenemy    Enemy\nplayer   Player, PurchaseOutcome\ngame     Game, attack\nmain     everything that prints\n</code></pre>\n<p>Look at the first group.</p>\n<p>Those eleven functions take plain values and return plain values.</p>\n<p>They never touch a <code>Player</code>, an <code>Enemy</code>, or a <code>Game</code>.</p>\n<p>They are the rules of the game, and every other group leans on them.</p>\n<p>Rust has a name for a named group of code: a <strong>module</strong>.</p>\n<p>For this project, we are going to give each top-level module its own file, one at a time, and run the tests after every move.</p>\n<hr />\n<h2 id=\"give-the-rules-their-own-module\">Give the rules their own module<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#give-the-rules-their-own-module\" aria-label=\"Link to this section\">#</a></h2>\n<p>Create a new file next to <code>main.rs</code>:</p>\n<pre><code class=\"language-text\">src/\n├── main.rs\n└── rules.rs\n</code></pre>\n<p>Cut the eleven rule functions out of <code>main.rs</code> and paste them into <code>rules.rs</code>.</p>\n<p>Leave <code>attack()</code> where it is.</p>\n<p>It takes a <code>Player</code> and an <code>Enemy</code>, so it is not one of our plain-value rules. It belongs with the game.</p>\n<p>The tests for those rules should move too.</p>\n<p>Nine of them check nothing but rules:</p>\n<pre><code class=\"language-text\">click_damage_adds_base_and_bonus\nhealth_never_drops_below_zero\nzero_health_is_defeated\nlater_stages_have_tougher_enemies\nlater_stages_pay_better\nlevels_need_more_xp_as_they_go\nlevels_make_the_player_hit_harder\nidle_damage_grows_with_level\nupgrades_cost_more_each_time\n</code></pre>\n<p>Cut each one out of the <code>tests</code> module in <code>main.rs</code>, including the <code>#[test]</code> line above it, and give <code>rules.rs</code> a <code>tests</code> module of its own at the bottom:</p>\n<pre><code class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n\n    // the nine rule tests go here\n}\n</code></pre>\n<p>The <code>use super::*;</code> line means:</p>\n<blockquote>\n<p>Bring in everything from the module above me.</p>\n</blockquote>\n<p>Now <code>main.rs</code> needs to find its rules again.</p>\n<p>Add this to the very top of <code>main.rs</code>, above everything else:</p>\n<pre><code class=\"language-rust\">mod rules;\n\nuse rules::{\n    base_damage_for_level, click_damage,\n    click_upgrade_bonus, click_upgrade_cost,\n    enemy_health_for_stage, gold_reward_for_stage,\n    health_after_hit, idle_damage_for_level,\n    is_defeated, xp_reward_for_stage,\n    xp_to_next_level,\n};\n</code></pre>\n<p><code>mod rules;</code> says:</p>\n<blockquote>\n<p>There is a module called <code>rules</code>, and its code lives in <code>src/rules.rs</code>.</p>\n</blockquote>\n<p>The <code>use</code> line brings its functions into <code>main.rs</code> by name, so the rest of the file can keep calling <code>click_damage()</code> exactly as before.</p>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>Rust refuses:</p>\n<pre><code class=\"language-text\">error[E0603]: function `base_damage_for_level` is private\n --&gt; src/main.rs:4:5\n  |\n4 |     base_damage_for_level,\n  |     ^^^^^^^^^^^^^^^^^^^^^ private function\n  |\nnote: the function `base_damage_for_level` is defined here\n --&gt; src/rules.rs:32:1\n  |\n32 | fn base_damage_for_level(level: u32) -&gt; u32 {\n  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n</code></pre>\n<p>You will get one of these for every function.</p>\n<p>That is not a bug in what we did.</p>\n<p>It is our first encounter with one of Rust's module boundaries:</p>\n<blockquote>\n<p><strong>By default, an item is visible inside its module and the modules beneath it. Sibling modules and outside code cannot use it unless we open that boundary.</strong></p>\n</blockquote>\n<p><code>main.rs</code> sits outside the <code>rules</code> module.</p>\n<p>It cannot reach those functions yet.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After we move the eleven functions into <code>rules.rs</code>, why does <code>main.rs</code> fail with <code>E0603</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>use</code> can only import structs, never functions</li>\n<li>Items are private by default, and <code>main.rs</code> sits outside the <code>rules</code> module</li>\n<li>A module can only be declared inside <code>lib.rs</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Items are private by default, and <code>main.rs</code> sits outside the <code>rules</code> module</p>\n<p>Privacy is the default. An item is visible inside its own module (and the modules beneath it) until we open it up with <code>pub</code>.</p>\n</div>\n</div>\n</section>\n<p>The simplest way to open that boundary is <code>pub</code>, short for <em>public</em>.</p>\n<p>Put it in front of each of the eleven functions:</p>\n<pre><code class=\"language-rust\">pub fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Twenty-one tests pass, and the output is the same as before.</p>\n<p>The count should stay at twenty-one after every move in this article.</p>\n<p>We moved eleven functions and nine tests into another file, and nothing changed.</p>\n<p>That is what a good move looks like.</p>\n<p>Try it: a real project puts <code>rules</code> in its own file, but a live example has only one file, so here the module is written inline with <code>mod rules { ... }</code>. It behaves the same way. Run it and you should see <code>Damage: 7</code> and <code>Health left: 23</code>. Then delete the <code>pub</code> in front of <code>click_damage</code> and run it again. That is the same <code>E0603</code> error we just fixed, on purpose this time. Put the <code>pub</code> back when you have read it.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try making it private</figcaption><pre><code class=\"language-rust\">mod rules {\n    pub fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n        base.saturating_add(bonus)\n    }\n\n    pub fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n        health.saturating_sub(damage)\n    }\n}\n\nuse rules::{click_damage, health_after_hit};\n\nfn main() {\n    let damage = click_damage(5, 2);\n    let health = health_after_hit(30, damage);\n\n    println!(&quot;Damage: {damage}&quot;);\n    println!(&quot;Health left: {health}&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\">the original article</a>.</p></figure>\n<hr />\n<h2 id=\"the-enemy\">The enemy<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-enemy\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>Enemy</code> is next because it is the smallest.</p>\n<p>Create <code>src/enemy.rs</code>, and move the <code>Enemy</code> struct and its <code>impl</code> block into it.</p>\n<p>The new file needs to find the rules it uses, so it starts like this:</p>\n<pre><code class=\"language-rust\">use crate::rules::{\n    enemy_health_for_stage,\n    gold_reward_for_stage,\n    xp_reward_for_stage,\n};\n</code></pre>\n<p><code>crate</code> means the top of the crate this file belongs to.</p>\n<p>A path beginning with <code>crate::</code> starts there no matter which module we are currently inside.</p>\n<p>For now, all of these modules still belong to the executable crate.</p>\n<p>In a moment we are going to move them into a library crate, and that distinction will matter.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Inside <code>enemy.rs</code>, what does the path <code>crate::rules::xp_reward_for_stage</code> mean?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Start from <code>enemy.rs</code> and look for a nested <code>rules</code> module inside it</li>\n<li>Start from the top of the crate and go to <code>rules</code>, no matter which module we are in</li>\n<li>Look for a <code>rules</code> crate that we installed from outside</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Start from the top of the crate and go to <code>rules</code>, no matter which module we are in</p>\n<p><code>crate::</code> always starts at the root of the crate the file belongs to, so the same path works from any module.</p>\n</div>\n</div>\n</section>\n<p>Back in <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">mod enemy;\n</code></pre>\n<p>under <code>mod rules;</code>, and:</p>\n<pre><code class=\"language-rust\">use enemy::Enemy;\n</code></pre>\n<p>under the rules import.</p>\n<p>Build again.</p>\n<p>Rust has an opinion:</p>\n<pre><code class=\"language-text\">error[E0603]: struct `Enemy` is private\n --&gt; src/main.rs:4:12\n  |\n4 | use enemy::Enemy;\n  |            ^^^^^ private struct\n</code></pre>\n<p>Same boundary, same first fix:</p>\n<pre><code class=\"language-rust\">pub struct Enemy\n</code></pre>\n<p>Build again, and Rust has a few more opinions.</p>\n<p>Making the struct public does not automatically make all of its fields public.</p>\n<p>Each field has its own visibility.</p>\n<p>So does <code>Enemy::for_stage()</code>.</p>\n<p>For now, here is the finished file:</p>\n<pre><code class=\"language-rust\">use crate::rules::{\n    enemy_health_for_stage,\n    gold_reward_for_stage,\n    xp_reward_for_stage,\n};\n\npub struct Enemy {\n    pub name: String,\n    pub health: u32,\n    pub gold_reward: u32,\n    pub xp_reward: u32,\n}\n\nimpl Enemy {\n    pub fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<p>Everything is <code>pub</code> for now.</p>\n<p>That is too generous.</p>\n<p>We are going to fix it on purpose once the boundaries are in place.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-one pass.</p>\n<p>You may also see warnings that some imports in <code>main.rs</code> are unused.</p>\n<p>That is true.</p>\n<p><code>Enemy</code> was the only caller of <code>enemy_health_for_stage()</code>, <code>gold_reward_for_stage()</code>, and <code>xp_reward_for_stage()</code>, and it just moved.</p>\n<p>Delete those names from the <code>use rules::{...}</code> list in <code>main.rs</code>.</p>\n<hr />\n<h2 id=\"the-player\">The player<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-player\" aria-label=\"Link to this section\">#</a></h2>\n<p>Same move, bigger file.</p>\n<p>Create <code>src/player.rs</code>.</p>\n<p>Move in:</p>\n<ul>\n<li><code>Player</code></li>\n<li><code>PurchaseOutcome</code></li>\n<li>the <code>impl Player</code> block</li>\n</ul>\n<p><code>PurchaseOutcome</code> goes with <code>Player</code> because it is the player's vocabulary.</p>\n<p>It is how a purchase answers.</p>\n<p>At the top of the file, import the rules <code>Player</code> uses:</p>\n<pre><code class=\"language-rust\">use crate::rules::{\n    base_damage_for_level,\n    click_upgrade_bonus,\n    click_upgrade_cost,\n    xp_to_next_level,\n};\n</code></pre>\n<p>Then add <code>pub</code> to the struct, its four fields, the enum, and every function in <code>impl Player</code>.</p>\n<p>An enum's variants follow the enum's visibility.</p>\n<p>Make <code>PurchaseOutcome</code> public, and its <code>Purchased</code> and <code>NotEnoughGold</code> variants are available with it.</p>\n<p>Four tests are about the player and nothing else:</p>\n<pre><code class=\"language-text\">leveling_up_keeps_leftover_xp\na_big_reward_can_skip_levels\nbuying_an_upgrade_needs_gold\nbuying_an_upgrade_spends_gold\n</code></pre>\n<p>Move those into a <code>tests</code> module at the bottom of <code>player.rs</code>, the same way we did with the rules.</p>\n<p>In <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">mod player;\n</code></pre>\n<p>and:</p>\n<pre><code class=\"language-rust\">use player::{Player, PurchaseOutcome};\n</code></pre>\n<p><code>Game</code> and <code>attack()</code> still live in <code>main.rs</code> for now, so <code>main.rs</code> still needs <code>Player</code>.</p>\n<p>That import drops out again in the next section.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-one pass.</p>\n<p>Clean up the new unused-import warnings the same way: remove <code>base_damage_for_level</code>, <code>click_upgrade_bonus</code>, and <code>click_upgrade_cost</code> from the <code>use rules::{...}</code> list in <code>main.rs</code>.</p>\n<hr />\n<h2 id=\"the-game\">The game<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-game\" aria-label=\"Link to this section\">#</a></h2>\n<p>Last one.</p>\n<p>Create <code>src/game.rs</code> and move in:</p>\n<ul>\n<li><code>Game</code></li>\n<li>its <code>impl</code> block</li>\n<li><code>attack()</code></li>\n</ul>\n<p>It needs three things from the other modules:</p>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\n    click_damage,\n    health_after_hit,\n    idle_damage_for_level,\n    is_defeated,\n};\n</code></pre>\n<p>Add <code>pub</code> to the struct and its three fields.</p>\n<p>Then add it to the four functions the outside world is going to call:</p>\n<pre><code class=\"language-rust\">pub fn new() -&gt; Game\npub fn click(&amp;mut self)\npub fn tick(&amp;mut self, seconds: u32)\npub fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome\n</code></pre>\n<p>Now look at what is <strong>not</strong> on that list.</p>\n<p><code>resolve_hit()</code> stays private.</p>\n<p><code>defeat_enemy()</code> stays private.</p>\n<p>So does <code>attack()</code>.</p>\n<p>Nothing outside <code>game.rs</code> has a reason to call them.</p>\n<p>When a slime dies, the game decides what that means.</p>\n<p>A caller should not get to say:</p>\n<blockquote>\n<p>Pretend that enemy just died.</p>\n</blockquote>\n<p>We will close the field loophole shortly.</p>\n<p>That is what privacy is for.</p>\n<p>It is not about hiding code for the sake of hiding it.</p>\n<p>It is about deciding what the rest of the program is allowed to depend on.</p>\n<p>Eight tests are about the game, including the one for <code>attack()</code>:</p>\n<pre><code class=\"language-text\">attack_reduces_enemy_health\na_new_game_starts_at_stage_one\nclicking_damages_the_enemy\ndefeating_an_enemy_pays_the_player\none_second_is_one_idle_hit\nzero_seconds_changes_nothing\nenough_time_defeats_the_enemy\none_long_tick_matches_many_short_ones\n</code></pre>\n<p>Move them to the bottom of <code>game.rs</code>.</p>\n<p>In <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">mod game;\n</code></pre>\n<p>and:</p>\n<pre><code class=\"language-rust\">use game::Game;\n</code></pre>\n<p>At this point, the <code>tests</code> module in <code>main.rs</code> is empty.</p>\n<p>Delete it.</p>\n<p>You can also remove <code>Player</code> from:</p>\n<pre><code class=\"language-rust\">use player::{Player, PurchaseOutcome};\n</code></pre>\n<p>because only <code>Game</code> needs it now.</p>\n<p>Clear out any remaining unused imports.</p>\n<p>The top of <code>main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\nuse game::Game;\nuse player::PurchaseOutcome;\nuse rules::{click_damage, xp_to_next_level};\n</code></pre>\n<p>Under that, only <code>main()</code> is left.</p>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Twenty-one tests.</p>\n<p>Same output.</p>\n<p>Four files hold the game.</p>\n<p>One holds the printing.</p>\n<p>But we have not built the wall yet.</p>\n<p><code>main.rs</code> still declares all four modules.</p>\n<p>They are part of the executable crate.</p>\n<p>Nothing outside that executable could use them as a library.</p>\n<hr />\n<h2 id=\"build-the-wall\">Build the wall<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#build-the-wall\" aria-label=\"Link to this section\">#</a></h2>\n<p>A Cargo package can contain both a library target and a binary target:</p>\n<pre><code class=\"language-text\">src/lib.rs    library\nsrc/main.rs   executable\n</code></pre>\n<p>Rust compiles them as separate crates.</p>\n<p>The binary can depend on the library.</p>\n<p>That gives us the wall we have been looking for.</p>\n<p>A <strong>crate</strong> is one compiled unit of Rust code.</p>\n<p>Until now, our project has had one crate: the executable.</p>\n<p>Now our package will contain two:</p>\n<pre><code class=\"language-text\">library crate\n    ↑\nbinary crate\n</code></pre>\n<p>They live in the same project, but Rust treats them as separate pieces of code.</p>\n<p>Create <code>src/lib.rs</code>, and move the four <code>mod</code> lines out of <code>main.rs</code> and into it:</p>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n</code></pre>\n<p>The project is named:</p>\n<pre><code class=\"language-text\">rustbound-idle\n</code></pre>\n<p>Rust turns the hyphen into an underscore when we refer to the library in code:</p>\n<pre><code class=\"language-text\">rustbound_idle\n</code></pre>\n<p>At the top of <code>main.rs</code>, replace all the old <code>mod</code> and <code>use</code> lines with:</p>\n<pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n</code></pre>\n<p>Build it:</p>\n<pre><code class=\"language-bash\">cargo build\n</code></pre>\n<p>Rust complains.</p>\n<p>The first problem looks like this:</p>\n<pre><code class=\"language-text\">error[E0432]: unresolved imports\n  `rustbound_idle::Game`,\n  `rustbound_idle::PurchaseOutcome`\n --&gt; src/main.rs:1:22\n  |\n  |   no `Game` in the root\n  |   no `PurchaseOutcome` in the root\n</code></pre>\n<p>The library has a <code>game</code> module.</p>\n<p>That module has a <code>Game</code>.</p>\n<p>But <code>mod game;</code> is private.</p>\n<p>From outside the library, there is no public path to <code>Game</code>.</p>\n<p>As far as the binary can tell, the library's front door is empty.</p>\n<p>We choose what appears there.</p>\n<p>That is what <code>pub use</code> is for:</p>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</code></pre>\n<p><code>pub use</code> means:</p>\n<blockquote>\n<p>Take this item from inside the library and make it available through this public path.</p>\n</blockquote>\n<p>Now code using the library can write:</p>\n<pre><code class=\"language-rust\">rustbound_idle::Game\n</code></pre>\n<p>Where <code>Game</code> actually lives inside the library is no longer the caller's problem.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>mod game;</code> in <code>lib.rs</code> contains a <code>pub struct Game</code>, yet <code>use rustbound_idle::Game;</code> fails. What is missing?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A <code>pub</code> in front of every field of <code>Game</code></li>\n<li>A public path to <code>Game</code>, such as <code>pub use game::Game;</code>, because <code>mod game;</code> is private</li>\n<li>A second <code>Game</code> inside <code>main.rs</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. A public path to <code>Game</code>, such as <code>pub use game::Game;</code>, because <code>mod game;</code> is private</p>\n<p>A <code>pub</code> item inside a private module is still unreachable from outside. <code>pub use</code> puts it on the library's front door.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: Open the front door</strong></p>\n<p>The <code>engine</code> module below hides its <code>enemy</code> module, so <code>main</code> cannot find <code>Enemy</code>. Add one <code>pub use</code> line where the <code>// TODO</code> is.</p>\n<p>Keep the <code>engine</code>, <code>rules</code> and <code>enemy</code> modules and the <code>Enemy</code> struct as they are, because the checks reach for <code>engine::Enemy</code>. <strong>Check my code</strong> will tell you how you are doing; until the line is right it will show the compiler's error instead.</p>\n<p><strong>Expected result:</strong> the starter stops with <code>error[E0432]: unresolved import</code> (the same front-door error as above). With your line added, it prints <code>Training Slime has 50 health</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Open the front door</figcaption><pre><code class=\"language-rust\">mod engine {\n    mod rules {\n        pub(crate) fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n            stage.saturating_mul(10).saturating_add(10)\n        }\n    }\n\n    mod enemy {\n        use crate::engine::rules::enemy_health_for_stage;\n\n        pub struct Enemy {\n            pub name: String,\n            pub health: u32,\n        }\n\n        impl Enemy {\n            pub fn for_stage(stage: u32) -&gt; Enemy {\n                Enemy {\n                    name: String::from(&quot;Training Slime&quot;),\n                    health: enemy_health_for_stage(stage),\n                }\n            }\n        }\n    }\n\n    // TODO: put Enemy on the front door with one `pub use` line\n}\n\nuse engine::Enemy;\n\nfn main() {\n    let enemy = Enemy::for_stage(4);\n\n    println!(&quot;{} has {} health&quot;, enemy.name, enemy.health);\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// From outside `engine`, we should be able to write `engine::Enemy` and build one. Look at who can see that name.\n#[test]\nfn enemy_is_reachable_through_engine() {\n    let enemy = crate::engine::Enemy::for_stage(4);\n    assert_eq!(enemy.health, 50);\n    assert_eq!(enemy.name, &quot;Training Slime&quot;);\n}\n\n/// Calling `main()` should work once the door is open. Every import in the file has to resolve.\n#[test]\nfn main_runs() {\n    main();\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Inside <code>engine</code>, the <code>enemy</code> module is right beside you, so the path is short: <code>pub use enemy::Enemy;</code>.</p>\n</aside>\n<p>Build again.</p>\n<p>Now the remaining errors are about rules:</p>\n<pre><code class=\"language-text\">error[E0425]: cannot find function `xp_to_next_level` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\n</code></pre>\n<p>Those calls used to work because <code>main.rs</code> imported the rules directly.</p>\n<p>Now <code>rules</code> is private inside the library.</p>\n<p>There are two ways we could fix that.</p>\n<p>We could put <code>rules</code> on the front door too:</p>\n<pre><code class=\"language-rust\">pub mod rules;\n</code></pre>\n<p>Then <code>main.rs</code> could import those formulas and keep doing this:</p>\n<pre><code class=\"language-rust\">click_damage(\n    game.player.base_damage(),\n    game.player.bonus_damage()\n)\n</code></pre>\n<p>That would compile.</p>\n<p>But think about what it means.</p>\n<p>Every formula in the game becomes part of the library's public API.</p>\n<p>And the screen keeps doing game arithmetic.</p>\n<p>Today the screen is a terminal.</p>\n<p>Later it will be a browser.</p>\n<p>If both of them know how to calculate damage, we now have multiple callers depending on an internal game rule.</p>\n<p>The rules should live in one place.</p>\n<p>The screen should ask for answers.</p>\n<p>So we take the second path.</p>\n<p>Leave <code>rules</code> private.</p>\n<p>Let <code>Player</code> provide the information the outside world needs.</p>\n<p>Add two methods inside <code>impl Player</code>, after <code>add_xp()</code>:</p>\n<pre><code class=\"language-rust\">pub fn xp_needed(&amp;self) -&gt; u32 {\n    xp_to_next_level(self.level)\n}\n\npub fn click_damage(&amp;self) -&gt; u32 {\n    click_damage(\n        self.base_damage(),\n        self.bonus_damage()\n    )\n}\n</code></pre>\n<p>The method and the rule share a name.</p>\n<p>That is fine.</p>\n<p>Inside the method:</p>\n<pre><code class=\"language-rust\">click_damage(...)\n</code></pre>\n<p>with no <code>self.</code> in front of it means the imported rule.</p>\n<p>The method is the player asking that rule a question about itself.</p>\n<p>First, add <code>click_damage</code> to the <code>use crate::rules::{...}</code> list at the top of <code>player.rs</code>.</p>\n<p>It was not there before because <code>Player</code> used to leave that arithmetic to <code>attack()</code>.</p>\n<p>While we are in there, <code>add_xp()</code> has been calling:</p>\n<pre><code class=\"language-rust\">xp_to_next_level(self.level)\n</code></pre>\n<p>twice.</p>\n<p>Now that the player has a method for that question, replace its loop with:</p>\n<pre><code class=\"language-rust\">while self.xp &gt;= self.xp_needed() {\n    self.xp -= self.xp_needed();\n    self.level = self.level.saturating_add(1);\n}\n</code></pre>\n<p>The loop is still the same loop.</p>\n<p><code>xp_to_next_level</code> stays imported because <code>xp_needed()</code> uses it.</p>\n<p><code>attack()</code> in <code>game.rs</code> can lean on the other new method too:</p>\n<pre><code class=\"language-rust\">fn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    enemy.health =\n        health_after_hit(\n            enemy.health,\n            player.click_damage()\n        );\n}\n</code></pre>\n<p>It no longer needs the <code>click_damage</code> rule, so remove that name from the <code>use</code> statement at the top of <code>game.rs</code>.</p>\n<p>Last, <code>main()</code> gets simpler.</p>\n<p>Where it said:</p>\n<pre><code class=\"language-rust\">xp_to_next_level(game.player.level)\n</code></pre>\n<p>it now says:</p>\n<pre><code class=\"language-rust\">game.player.xp_needed()\n</code></pre>\n<p>And where it said:</p>\n<pre><code class=\"language-rust\">click_damage(\n    game.player.base_damage(),\n    game.player.bonus_damage()\n)\n</code></pre>\n<p>it now says:</p>\n<pre><code class=\"language-rust\">game.player.click_damage()\n</code></pre>\n<p>That second replacement happens twice in <code>main()</code>.</p>\n<p>Build, test, and run:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Same twenty-one tests.</p>\n<p>Same output.</p>\n<p>Cargo also prints test blocks for <code>main.rs</code> and doc-tests that say:</p>\n<pre><code class=\"language-text\">running 0 tests\n</code></pre>\n<p>That is normal.</p>\n<p>And <code>main.rs</code> no longer knows a single game formula.</p>\n<hr />\n<h2 id=\"make-the-front-door-smaller\">Make the front door smaller<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#make-the-front-door-smaller\" aria-label=\"Link to this section\">#</a></h2>\n<p>We said everything was too <code>pub</code>.</p>\n<p>Time to fix that.</p>\n<p>Right now <code>main()</code> can see far more than it needs.</p>\n<p>Try it.</p>\n<p>Add this line just after:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n</code></pre>\n<pre><code class=\"language-rust\">game.player.add_gold(1_000_000);\n</code></pre>\n<p>It compiles.</p>\n<p>A screen can hand the player a million gold, and the game never gets a say.</p>\n<p>Let us close that off.</p>\n<p>Rust has a visibility setting between fully private and fully public:</p>\n<pre><code class=\"language-rust\">pub(crate)\n</code></pre>\n<p>It means:</p>\n<blockquote>\n<p>Visible anywhere inside this crate, but not outside it.</p>\n</blockquote>\n<p>Our library can use the item freely.</p>\n<p><code>main.rs</code> belongs to a different crate now, so it cannot.</p>\n<p>Go through the files and tighten everything <code>main()</code> does not need.</p>\n<p>The rule for now is:</p>\n<blockquote>\n<p><code>pub</code> for what outside code needs. <code>pub(crate)</code> or private for everything else.</p>\n</blockquote>\n<p>In <code>rules.rs</code>, change all eleven:</p>\n<pre><code class=\"language-rust\">pub fn\n</code></pre>\n<p>to:</p>\n<pre><code class=\"language-rust\">pub(crate) fn\n</code></pre>\n<p>In <code>enemy.rs</code>:</p>\n<ul>\n<li>change <code>for_stage()</code> to <code>pub(crate)</code></li>\n<li>make <code>gold_reward</code> and <code>xp_reward</code> <code>pub(crate)</code></li>\n<li>leave <code>name</code> and <code>health</code> public because <code>main()</code> displays them</li>\n</ul>\n<p>In <code>player.rs</code>, change:</p>\n<ul>\n<li><code>new</code></li>\n<li><code>add_gold</code></li>\n<li><code>add_xp</code></li>\n<li><code>buy_click_upgrade</code></li>\n</ul>\n<p>to <code>pub(crate)</code>.</p>\n<p>Take <code>pub</code> off <code>base_damage()</code> and <code>bonus_damage()</code> entirely.</p>\n<p>Only <code>Player</code> needs those now.</p>\n<p>What remains fully public is what the current screen needs:</p>\n<pre><code class=\"language-text\">Game\n  new\n  click\n  tick\n  buy_click_upgrade\n  stage\n  player\n  enemy\n\nPlayer\n  gold\n  xp\n  level\n  click_upgrade_level\n  xp_needed\n  click_damage\n\nEnemy\n  name\n  health\n\nPurchaseOutcome\n</code></pre>\n<p>Now try this again:</p>\n<pre><code class=\"language-rust\">game.player.add_gold(1_000_000);\n</code></pre>\n<p>It fails.</p>\n<p><code>add_gold()</code> is <code>pub(crate)</code>, and <code>main.rs</code> is outside the library crate.</p>\n<p>Delete that line.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\ncargo run\n</code></pre>\n<p>Nothing changes.</p>\n<p>That is the point.</p>\n<p>If tightening the boundary had broken <code>main()</code>, it would mean <code>main()</code> depended on something we did not actually want to promise publicly.</p>\n<p>Now try to cheat another way.</p>\n<p>In <code>main()</code>, add:</p>\n<pre><code class=\"language-rust\">let secret = game.enemy.gold_reward;\n</code></pre>\n<p>Rust says no:</p>\n<pre><code class=\"language-text\">error[E0616]: field `gold_reward` of struct `Enemy` is private\n</code></pre>\n<p>Delete it.</p>\n<p>Now try reaching for an internal damage detail.</p>\n<p>Replace:</p>\n<pre><code class=\"language-rust\">game.player.click_damage()\n</code></pre>\n<p>with:</p>\n<pre><code class=\"language-rust\">game.player.base_damage()\n</code></pre>\n<p>Rust refuses again:</p>\n<pre><code class=\"language-text\">error[E0624]: method `base_damage` is private\n</code></pre>\n<p>Put it back.</p>\n<p>Those errors are the wall doing its job.</p>\n<p>Try it: here is the same idea in one file. <code>base_damage</code> is private to the <code>player</code> module, and <code>click_damage</code> is the public question the outside world is allowed to ask. Run it and you should see <code>Click damage: 7</code>. Then remove the <code>//</code> in front of the last <code>println!</code> in <code>main</code> and run again. You should get <code>E0624</code>, <code>method base_damage is private</code>, which is exactly the error above.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · reach for the private method</figcaption><pre><code class=\"language-rust\">mod player {\n    pub struct Player {\n        pub level: u32,\n    }\n\n    impl Player {\n        fn base_damage(&amp;self) -&gt; u32 {\n            self.level + 4\n        }\n\n        pub fn click_damage(&amp;self) -&gt; u32 {\n            self.base_damage()\n        }\n    }\n}\n\nuse player::Player;\n\nfn main() {\n    let player = Player { level: 3 };\n\n    println!(&quot;Click damage: {}&quot;, player.click_damage());\n    // println!(&quot;Base damage: {}&quot;, player.base_damage());\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Once <code>add_gold()</code> is <code>pub(crate)</code>, why can <code>main()</code> no longer call it?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>pub(crate)</code> is the same as private, so only <code>Player</code> itself can call it</li>\n<li><code>main.rs</code> is now a different crate from the library, and <code>pub(crate)</code> only reaches code inside the same crate</li>\n<li>Methods can never be called on a field like <code>game.player</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>main.rs</code> is now a different crate from the library, and <code>pub(crate)</code> only reaches code inside the same crate</p>\n<p>The rest of the library can still call <code>add_gold()</code>. Only code outside the crate, like our binary, is shut out.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"a-test-from-the-outside\">A test from the outside<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#a-test-from-the-outside\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our twenty-one tests all live <strong>inside</strong> the library, next to the code they check.</p>\n<p>That is useful.</p>\n<p>Those tests can work closely with the modules they belong to.</p>\n<p>But it leaves another question:</p>\n<blockquote>\n<p>Does the library work for code that only gets to use the public front door?</p>\n</blockquote>\n<p>Cargo has a place for that.</p>\n<p>Create a folder called <code>tests</code> next to <code>src</code>, and put a file in it:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── src/\n└── tests/\n    └── idle_hour.rs\n</code></pre>\n<p>Each Rust file directly inside <code>tests/</code> is compiled as a separate <strong>integration test crate</strong>.</p>\n<p>It uses the library the same way outside code does: through the public API.</p>\n<p>This is the first code in the project that sees the game the way a stranger would.</p>\n<p>Put this in <code>tests/idle_hour.rs</code>:</p>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn an_hour_away_moves_the_game_forward() {\n    let mut game = Game::new();\n\n    game.tick(3600);\n\n    assert_eq!(game.stage, 128);\n    assert_eq!(game.player.level, 47);\n}\n</code></pre>\n<p>The terminal program clicked through three stages before it waited an hour.</p>\n<p>This test waits an hour from a brand-new game, so the numbers are different:</p>\n<pre><code class=\"language-text\">stage 128\nlevel 47\n</code></pre>\n<p>Run everything:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Cargo prints a block for each piece it tests.</p>\n<p>You should see:</p>\n<ul>\n<li>the twenty-one tests inside the library</li>\n<li>the new integration test in <code>tests/idle_hour.rs</code></li>\n<li>blocks for <code>main.rs</code> and doc-tests that say <code>running 0 tests</code></li>\n</ul>\n<p>That is fine.</p>\n<p>Twenty-two tests in all.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why can <code>tests/idle_hour.rs</code> not call a <code>pub(crate)</code> function from the library?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Integration tests cannot call any functions at all</li>\n<li>Each file in <code>tests/</code> is its own crate and sees only the public API, like any outside code</li>\n<li><code>pub(crate)</code> items are removed when tests are compiled</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Each file in <code>tests/</code> is its own crate and sees only the public API, like any outside code</p>\n<p>That is what makes it a test from the outside: if it compiles there, a stranger could write the same code.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"one-last-complaint\">One last complaint<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#one-last-complaint\" aria-label=\"Link to this section\">#</a></h2>\n<p>There is one more thing, and it has been waiting for us.</p>\n<p>Clippy is Rust's linter.</p>\n<p>It looks for patterns that compile but could be clearer, more idiomatic, or less error-prone.</p>\n<p>If:</p>\n<pre><code class=\"language-bash\">cargo clippy\n</code></pre>\n<p>is not available, install the component first:</p>\n<pre><code class=\"language-bash\">rustup component add clippy\n</code></pre>\n<p>Then run:</p>\n<pre><code class=\"language-bash\">cargo clippy\n</code></pre>\n<p>Clippy has a suggestion:</p>\n<pre><code class=\"language-text\">warning: you should consider adding a `Default` implementation for `Game`\n</code></pre>\n<p>It did not complain before because <code>Game::new()</code> was not part of a public API.</p>\n<p>Now <code>Game</code> is public, and so is its zero-argument <code>new()</code>.</p>\n<p>Clippy notices that callers may reasonably expect the standard Rust <code>Default</code> trait too.</p>\n<p><code>Default</code> describes a type's standard starting value.</p>\n<p>We already have one:</p>\n<pre><code class=\"language-rust\">Game::new()\n</code></pre>\n<p>So we can say that explicitly:</p>\n<pre><code class=\"language-rust\">impl Default for Game {\n    fn default() -&gt; Game {\n        Game::new()\n    }\n}\n</code></pre>\n<p>Put it in <code>game.rs</code>, between the <code>impl Game</code> block and <code>attack()</code>.</p>\n<p>Now:</p>\n<pre><code class=\"language-rust\">Game::new()\n</code></pre>\n<p>and:</p>\n<pre><code class=\"language-rust\">Game::default()\n</code></pre>\n<p>produce the same starting game.</p>\n<p>Run Clippy again.</p>\n<p>It should be quiet.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>Your project should now look like this:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   └── game.rs\n└── tests/\n    └── idle_hour.rs\n</code></pre>\n<details>\n<summary><code>src/lib.rs</code> · 8 lines</summary>\n<pre><code class=\"language-rust\">mod enemy;\nmod game;\nmod player;\nmod rules;\n\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</code></pre>\n</details>\n<details>\n<summary><code>src/main.rs</code> · 62 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::{Game, PurchaseOutcome};\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                game.player.xp_needed()\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    game.player.click_damage()\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name,\n                game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                game.player.click_damage(),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage,\n        game.stage,\n        level,\n        game.player.level,\n        gold,\n        game.player.gold\n    );\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/rules.rs</code> · 107 lines</summary>\n<pre><code class=\"language-rust\">pub(crate) fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\npub(crate) fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\npub(crate) fn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\npub(crate) fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\npub(crate) fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\npub(crate) fn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\npub(crate) fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\npub(crate) fn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\npub(crate) fn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\npub(crate) fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\npub(crate) fn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades\n        .saturating_add(1)\n        .saturating_mul(10)\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/enemy.rs</code> · 23 lines</summary>\n<pre><code class=\"language-rust\">use crate::rules::{\n    enemy_health_for_stage,\n    gold_reward_for_stage,\n    xp_reward_for_stage,\n};\n\npub struct Enemy {\n    pub name: String,\n    pub health: u32,\n    pub(crate) gold_reward: u32,\n    pub(crate) xp_reward: u32,\n}\n\nimpl Enemy {\n    pub(crate) fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/player.rs</code> · 129 lines</summary>\n<pre><code class=\"language-rust\">use crate::rules::{\n    base_damage_for_level,\n    click_damage,\n    click_upgrade_bonus,\n    click_upgrade_cost,\n    xp_to_next_level,\n};\n\npub struct Player {\n    pub gold: u32,\n    pub xp: u32,\n    pub level: u32,\n    pub click_upgrade_level: u32,\n}\n\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    pub(crate) fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    pub(crate) fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    pub(crate) fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= self.xp_needed() {\n            self.xp -= self.xp_needed();\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    pub fn xp_needed(&amp;self) -&gt; u32 {\n        xp_to_next_level(self.level)\n    }\n\n    pub fn click_damage(&amp;self) -&gt; u32 {\n        click_damage(\n            self.base_damage(),\n            self.bonus_damage()\n        )\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    pub(crate) fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost =\n            click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level =\n            self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::NotEnoughGold\n        );\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(\n            player.buy_click_upgrade(),\n            PurchaseOutcome::Purchased\n        );\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>src/game.rs</code> · 186 lines</summary>\n<pre><code class=\"language-rust\">use crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\n    health_after_hit,\n    idle_damage_for_level,\n    is_defeated,\n};\n\npub struct Game {\n    pub stage: u32,\n    pub player: Player,\n    pub enemy: Enemy,\n}\n\nimpl Game {\n    pub fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    pub fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    pub fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage =\n                idle_damage_for_level(self.player.level);\n\n            self.enemy.health =\n                health_after_hit(\n                    self.enemy.health,\n                    damage\n                );\n\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    pub fn buy_click_upgrade(\n        &amp;mut self\n    ) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nimpl Default for Game {\n    fn default() -&gt; Game {\n        Game::new()\n    }\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    enemy.health =\n        health_after_hit(\n            enemy.health,\n            player.click_damage()\n        );\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(\n            long.enemy.health,\n            short.enemy.health\n        );\n        assert_eq!(\n            long.player.gold,\n            short.player.gold\n        );\n        assert_eq!(\n            long.player.xp,\n            short.player.xp\n        );\n        assert_eq!(\n            long.player.level,\n            short.player.level\n        );\n    }\n}\n</code></pre>\n</details>\n<details>\n<summary><code>tests/idle_hour.rs</code> · 11 lines</summary>\n<pre><code class=\"language-rust\">use rustbound_idle::Game;\n\n#[test]\nfn an_hour_away_moves_the_game_forward() {\n    let mut game = Game::new();\n\n    game.tick(3600);\n\n    assert_eq!(game.stage, 128);\n    assert_eq!(game.player.level, 47);\n}\n</code></pre>\n</details>\n<p>Run everything:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\ncargo clippy\n</code></pre>\n<p>The game should clear three stages, buy one upgrade, and spend an hour away.</p>\n<p>All twenty-two tests should pass.</p>\n<p>Clippy should be quiet.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>modules, and <code>mod</code> to declare one</li>\n<li>organizing top-level modules into separate files</li>\n<li><code>use</code>, and <code>crate::</code> paths</li>\n<li>Rust's default privacy boundaries</li>\n<li><code>pub</code></li>\n<li><code>pub(crate)</code></li>\n<li><code>pub use</code>, for choosing what appears on a library's front door</li>\n<li>a library crate and a binary crate in the same Cargo package</li>\n<li>tests that live beside their code</li>\n<li>integration tests in <code>tests/</code></li>\n<li><code>Default</code>, which Clippy asked us to provide</li>\n</ul>\n<p>Two ideas are worth keeping.</p>\n<p>The first is that privacy is not about hiding.</p>\n<p>It is about deciding what everyone else is allowed to depend on.</p>\n<p>A private function gives us freedom.</p>\n<p>We can rename it, reorganize it, or replace its implementation without outside code depending on that detail, as long as the public behavior stays the same.</p>\n<p><code>resolve_hit()</code> can move.</p>\n<p><code>base_damage()</code> can change.</p>\n<p>Code outside their visibility boundary does not get to build itself around those details.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does keeping <code>resolve_hit()</code> private buy us?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It runs faster than a public function</li>\n<li>We can change or move it later without breaking any code outside its module</li>\n<li>It stops the compiler from warning about unused code</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. We can change or move it later without breaking any code outside its module</p>\n<p>A public item is a promise to everyone who uses it. A private one is a detail we are free to change.</p>\n</div>\n</div>\n</section>\n<p>The second idea is where the line went.</p>\n<p>We did not decide to build an engine.</p>\n<p>We sorted the code, and the sorting kept asking one question:</p>\n<blockquote>\n<p><em>Does this need to know about a screen?</em></p>\n</blockquote>\n<p>For everything except <code>main()</code>, the answer was no.</p>\n<p>That is the heart of what our little game engine is becoming.</p>\n<p>It is not a graphics library.</p>\n<p>It does not open a window.</p>\n<p>It does not print to a terminal.</p>\n<p>It does not know whether a browser even exists.</p>\n<p>It owns rules and state.</p>\n<p>The game's four main commands are:</p>\n<pre><code class=\"language-text\">new\nclick\ntick\nbuy_click_upgrade\n</code></pre>\n<p>We still expose some state so <code>main()</code> can display it.</p>\n<p>That part of the front door is wider than I want.</p>\n<p><code>Game</code>, <code>Player</code>, and <code>Enemy</code> still have public fields, which means <code>main()</code> could write:</p>\n<pre><code class=\"language-rust\">game.enemy.health = 0;\n</code></pre>\n<p>and the compiler would allow it.</p>\n<p>That is a loose end.</p>\n<p>We are going to lean on it soon.</p>\n<hr />\n<h2 id=\"it-cannot-remember\">It cannot remember<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#it-cannot-remember\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game can simulate an hour instantly.</p>\n<p>It cannot survive the program ending.</p>\n<p>Close the terminal and everything is gone:</p>\n<pre><code class=\"language-text\">stage\ngold\nXP\nlevel\nupgrades\n</code></pre>\n<p>An idle game that forgets you the moment you leave is not much of an idle game.</p>\n<p>Next time, the game learns to save itself.</p>\n<p>Not to a file.</p>\n<p>Not to a browser.</p>\n<p>Not to a server.</p>\n<p>The library will not know where saves go, and it should not.</p>\n<p>It will only know how to say, in a form that can be written down:</p>\n<blockquote>\n<p>This is everything about me.</p>\n</blockquote>\n<p>And later, given that same form:</p>\n<blockquote>\n<p>I remember.</p>\n</blockquote>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "We Accidentally Built a Game Engine.\nCanonical: https://mnixblog.netlify.app/rust-modules-library-crate-split-game/\nSplit Your Rust Game Into Modules and a Library\nOur game works, but its rules and its screen are tangled together in one file of more than four hundred lines. Today we pull them apart, so the same game can run in a terminal now and in a browser later.\nNothing about the game changes. We will not touch a rule.\nNobody planned that file. Each article needed one more thing, and each thing went in the next free spot. But look at what is in there: everything above main() knows nothing about a terminal or a screen. Only main() does.\nSo we put a wall between them. When we are done, main.rs is fifty-six lines: it creates a game, clicks, and prints. The game itself lives in a library that has never heard of a terminal.\nA library like that, made of rules and state that do not know how they are being displayed, is the heart of what people call a game engine.\nA small one.\nOurs will have a front door and nothing else.\nThe program still prints the same thing, a few stages cleared and then an hour of waiting:\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nSame output.\nSame twenty-one tests, plus one new one.\nDifferent house.\nOur starting point\nThis series builds a small idle game: a player clicks to fight slimes, earns gold and XP, and keeps progressing while you are away.\nIf you are only reading along, skip past this code to \"The groups are already visible.\"\nIf you are building along, create a project with cargo new rustbound-idle if you do not already have one. The name matters later.\nAt this point, src/main.rs holds three structs, an enum, three impl blocks, twelve small functions, and twenty-one tests:\n<details>\n<summary><code>src/main.rs</code> · 428-line starting point</summary>\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::NotEnoughGold\n);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::Purchased\n);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\n}\n</details>\nRun it, and the player clears three stages, buys an upgrade, and spends an hour away.\nRun cargo test, and all twenty-one tests pass.\nYou need a recent Rust version. This project uses edition 2024, which requires Rust 1.85 or newer.\nHold on to that test count.\nEverything we do today is a reorganization, and the tests are how we prove we did not break anything.\nThe groups are already visible\nNobody has to invent categories for this code.\nIt has been sorting itself since the first article:\nrules    click_damage, health_after_hit, is_defeated,\nenemy_health_for_stage, gold_reward_for_stage,\nxp_reward_for_stage, xp_to_next_level,\nbase_damage_for_level, idle_damage_for_level,\nclick_upgrade_bonus, click_upgrade_cost\nenemy    Enemy\nplayer   Player, PurchaseOutcome\ngame     Game, attack\nmain     everything that prints\nLook at the first group.\nThose eleven functions take plain values and return plain values.\nThey never touch a Player, an Enemy, or a Game.\nThey are the rules of the game, and every other group leans on them.\nRust has a name for a named group of code: a module.\nFor this project, we are going to give each top-level module its own file, one at a time, and run the tests after every move.\nGive the rules their own module\nCreate a new file next to main.rs:\nsrc/\n├── main.rs\n└── rules.rs\nCut the eleven rule functions out of main.rs and paste them into rules.rs.\nLeave attack() where it is.\nIt takes a Player and an Enemy, so it is not one of our plain-value rules. It belongs with the game.\nThe tests for those rules should move too.\nNine of them check nothing but rules:\nclick_damage_adds_base_and_bonus\nhealth_never_drops_below_zero\nzero_health_is_defeated\nlater_stages_have_tougher_enemies\nlater_stages_pay_better\nlevels_need_more_xp_as_they_go\nlevels_make_the_player_hit_harder\nidle_damage_grows_with_level\nupgrades_cost_more_each_time\nCut each one out of the tests module in main.rs, including the #[test] line above it, and give rules.rs a tests module of its own at the bottom:\n#[cfg(test)]\nmod tests {\nuse super::*;\n// the nine rule tests go here\n}\nThe use super::*; line means:\nBring in everything from the module above me.\nNow main.rs needs to find its rules again.\nAdd this to the very top of main.rs, above everything else:\nmod rules;\nuse rules::{\nbase_damage_for_level, click_damage,\nclick_upgrade_bonus, click_upgrade_cost,\nenemy_health_for_stage, gold_reward_for_stage,\nhealth_after_hit, idle_damage_for_level,\nis_defeated, xp_reward_for_stage,\nxp_to_next_level,\n};\nmod rules; says:\nThere is a module called rules, and its code lives in src/rules.rs.\nThe use line brings its functions into main.rs by name, so the rest of the file can keep calling click_damage() exactly as before.\nBuild it:\ncargo build\nRust refuses:\nerror[E0603]: function `base_damage_for_level` is private\n--> src/main.rs:4:5\n|\n4 |     base_damage_for_level,\n|     ^^^^^^^^^^^^^^^^^^^^^ private function\n|\nnote: the function `base_damage_for_level` is defined here\n--> src/rules.rs:32:1\n|\n32 | fn base_damage_for_level(level: u32) -> u32 {\n| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\nYou will get one of these for every function.\nThat is not a bug in what we did.\nIt is our first encounter with one of Rust's module boundaries:\nBy default, an item is visible inside its module and the modules beneath it. Sibling modules and outside code cannot use it unless we open that boundary.\nmain.rs sits outside the rules module.\nIt cannot reach those functions yet.\nQuestion: After we move the eleven functions into rules.rs, why does main.rs fail with E0603?\nuse can only import structs, never functions\nItems are private by default, and main.rs sits outside the rules module\nA module can only be declared inside lib.rs\nAnswer: 2. Items are private by default, and main.rs sits outside the rules module\nExplanation: Privacy is the default. An item is visible inside its own module (and the modules beneath it) until we open it up with pub.\nThe simplest way to open that boundary is pub, short for public.\nPut it in front of each of the eleven functions:\npub fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nRun both:\ncargo test\ncargo run\nTwenty-one tests pass, and the output is the same as before.\nThe count should stay at twenty-one after every move in this article.\nWe moved eleven functions and nine tests into another file, and nothing changed.\nThat is what a good move looks like.\nTry it: a real project puts rules in its own file, but a live example has only one file, so here the module is written inline with mod rules { ... }. It behaves the same way. Run it and you should see Damage: 7 and Health left: 23. Then delete the pub in front of click_damage and run it again. That is the same E0603 error we just fixed, on purpose this time. Put the pub back when you have read it.\nLive example · try making it private\nmod rules {\npub fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\npub fn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\n}\nuse rules::{click_damage, health_after_hit};\nfn main() {\nlet damage = click_damage(5, 2);\nlet health = health_after_hit(30, damage);\nprintln!(\"Damage: {damage}\");\nprintln!(\"Health left: {health}\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/).\nThe enemy\nEnemy is next because it is the smallest.\nCreate src/enemy.rs, and move the Enemy struct and its impl block into it.\nThe new file needs to find the rules it uses, so it starts like this:\nuse crate::rules::{\nenemy_health_for_stage,\ngold_reward_for_stage,\nxp_reward_for_stage,\n};\ncrate means the top of the crate this file belongs to.\nA path beginning with crate:: starts there no matter which module we are currently inside.\nFor now, all of these modules still belong to the executable crate.\nIn a moment we are going to move them into a library crate, and that distinction will matter.\nQuestion: Inside enemy.rs, what does the path crate::rules::xp_reward_for_stage mean?\nStart from enemy.rs and look for a nested rules module inside it\nStart from the top of the crate and go to rules, no matter which module we are in\nLook for a rules crate that we installed from outside\nAnswer: 2. Start from the top of the crate and go to rules, no matter which module we are in\nExplanation: crate:: always starts at the root of the crate the file belongs to, so the same path works from any module.\nBack in main.rs, add:\nmod enemy;\nunder mod rules;, and:\nuse enemy::Enemy;\nunder the rules import.\nBuild again.\nRust has an opinion:\nerror[E0603]: struct `Enemy` is private\n--> src/main.rs:4:12\n|\n4 | use enemy::Enemy;\n|            ^^^^^ private struct\nSame boundary, same first fix:\npub struct Enemy\nBuild again, and Rust has a few more opinions.\nMaking the struct public does not automatically make all of its fields public.\nEach field has its own visibility.\nSo does Enemy::for_stage().\nFor now, here is the finished file:\nuse crate::rules::{\nenemy_health_for_stage,\ngold_reward_for_stage,\nxp_reward_for_stage,\n};\npub struct Enemy {\npub name: String,\npub health: u32,\npub gold_reward: u32,\npub xp_reward: u32,\n}\nimpl Enemy {\npub fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nEverything is pub for now.\nThat is too generous.\nWe are going to fix it on purpose once the boundaries are in place.\nRun:\ncargo test\nTwenty-one pass.\nYou may also see warnings that some imports in main.rs are unused.\nThat is true.\nEnemy was the only caller of enemy_health_for_stage(), gold_reward_for_stage(), and xp_reward_for_stage(), and it just moved.\nDelete those names from the use rules::{...} list in main.rs.\nThe player\nSame move, bigger file.\nCreate src/player.rs.\nMove in:\nPlayer\nPurchaseOutcome\nthe impl Player block\nPurchaseOutcome goes with Player because it is the player's vocabulary.\nIt is how a purchase answers.\nAt the top of the file, import the rules Player uses:\nuse crate::rules::{\nbase_damage_for_level,\nclick_upgrade_bonus,\nclick_upgrade_cost,\nxp_to_next_level,\n};\nThen add pub to the struct, its four fields, the enum, and every function in impl Player.\nAn enum's variants follow the enum's visibility.\nMake PurchaseOutcome public, and its Purchased and NotEnoughGold variants are available with it.\nFour tests are about the player and nothing else:\nleveling_up_keeps_leftover_xp\na_big_reward_can_skip_levels\nbuying_an_upgrade_needs_gold\nbuying_an_upgrade_spends_gold\nMove those into a tests module at the bottom of player.rs, the same way we did with the rules.\nIn main.rs, add:\nmod player;\nand:\nuse player::{Player, PurchaseOutcome};\nGame and attack() still live in main.rs for now, so main.rs still needs Player.\nThat import drops out again in the next section.\nRun:\ncargo test\nTwenty-one pass.\nClean up the new unused-import warnings the same way: remove base_damage_for_level, click_upgrade_bonus, and click_upgrade_cost from the use rules::{...} list in main.rs.\nThe game\nLast one.\nCreate src/game.rs and move in:\nGame\nits impl block\nattack()\nIt needs three things from the other modules:\nuse crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\nclick_damage,\nhealth_after_hit,\nidle_damage_for_level,\nis_defeated,\n};\nAdd pub to the struct and its three fields.\nThen add it to the four functions the outside world is going to call:\npub fn new() -> Game\npub fn click(&mut self)\npub fn tick(&mut self, seconds: u32)\npub fn buy_click_upgrade(&mut self) -> PurchaseOutcome\nNow look at what is not on that list.\nresolve_hit() stays private.\ndefeat_enemy() stays private.\nSo does attack().\nNothing outside game.rs has a reason to call them.\nWhen a slime dies, the game decides what that means.\nA caller should not get to say:\nPretend that enemy just died.\nWe will close the field loophole shortly.\nThat is what privacy is for.\nIt is not about hiding code for the sake of hiding it.\nIt is about deciding what the rest of the program is allowed to depend on.\nEight tests are about the game, including the one for attack():\nattack_reduces_enemy_health\na_new_game_starts_at_stage_one\nclicking_damages_the_enemy\ndefeating_an_enemy_pays_the_player\none_second_is_one_idle_hit\nzero_seconds_changes_nothing\nenough_time_defeats_the_enemy\none_long_tick_matches_many_short_ones\nMove them to the bottom of game.rs.\nIn main.rs, add:\nmod game;\nand:\nuse game::Game;\nAt this point, the tests module in main.rs is empty.\nDelete it.\nYou can also remove Player from:\nuse player::{Player, PurchaseOutcome};\nbecause only Game needs it now.\nClear out any remaining unused imports.\nThe top of main.rs should look like this:\nmod enemy;\nmod game;\nmod player;\nmod rules;\nuse game::Game;\nuse player::PurchaseOutcome;\nuse rules::{click_damage, xp_to_next_level};\nUnder that, only main() is left.\nRun both:\ncargo test\ncargo run\nTwenty-one tests.\nSame output.\nFour files hold the game.\nOne holds the printing.\nBut we have not built the wall yet.\nmain.rs still declares all four modules.\nThey are part of the executable crate.\nNothing outside that executable could use them as a library.\nBuild the wall\nA Cargo package can contain both a library target and a binary target:\nsrc/lib.rs    library\nsrc/main.rs   executable\nRust compiles them as separate crates.\nThe binary can depend on the library.\nThat gives us the wall we have been looking for.\nA crate is one compiled unit of Rust code.\nUntil now, our project has had one crate: the executable.\nNow our package will contain two:\nlibrary crate\n↑\nbinary crate\nThey live in the same project, but Rust treats them as separate pieces of code.\nCreate src/lib.rs, and move the four mod lines out of main.rs and into it:\nmod enemy;\nmod game;\nmod player;\nmod rules;\nThe project is named:\nrustbound-idle\nRust turns the hyphen into an underscore when we refer to the library in code:\nrustbound_idle\nAt the top of main.rs, replace all the old mod and use lines with:\nuse rustbound_idle::{Game, PurchaseOutcome};\nBuild it:\ncargo build\nRust complains.\nThe first problem looks like this:\nerror[E0432]: unresolved imports\n`rustbound_idle::Game`,\n`rustbound_idle::PurchaseOutcome`\n--> src/main.rs:1:22\n|\n|   no `Game` in the root\n|   no `PurchaseOutcome` in the root\nThe library has a game module.\nThat module has a Game.\nBut mod game; is private.\nFrom outside the library, there is no public path to Game.\nAs far as the binary can tell, the library's front door is empty.\nWe choose what appears there.\nThat is what pub use is for:\nmod enemy;\nmod game;\nmod player;\nmod rules;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\npub use means:\nTake this item from inside the library and make it available through this public path.\nNow code using the library can write:\nrustbound_idle::Game\nWhere Game actually lives inside the library is no longer the caller's problem.\nQuestion: mod game; in lib.rs contains a pub struct Game, yet use rustbound_idle::Game; fails. What is missing?\nA pub in front of every field of Game\nA public path to Game, such as pub use game::Game;, because mod game; is private\nA second Game inside main.rs\nAnswer: 2. A public path to Game, such as pub use game::Game;, because mod game; is private\nExplanation: A pub item inside a private module is still unreachable from outside. pub use puts it on the library's front door.\nCoding challenge: Open the front door\nThe engine module below hides its enemy module, so main cannot find Enemy. Add one pub use line where the // TODO is.\nKeep the engine, rules and enemy modules and the Enemy struct as they are, because the checks reach for engine::Enemy. Check my code will tell you how you are doing; until the line is right it will show the compiler's error instead.\nExpected result: the starter stops with error[E0432]: unresolved import (the same front-door error as above). With your line added, it prints Training Slime has 50 health.\nOpen the front door\nmod engine {\nmod rules {\npub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\n}\nmod enemy {\nuse crate::engine::rules::enemy_health_for_stage;\npub struct Enemy {\npub name: String,\npub health: u32,\n}\nimpl Enemy {\npub fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\n}\n// TODO: put Enemy on the front door with one `pub use` line\n}\nuse engine::Enemy;\nfn main() {\nlet enemy = Enemy::for_stage(4);\nprintln!(\"{} has {} health\", enemy.name, enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// From outside `engine`, we should be able to write `engine::Enemy` and build one. Look at who can see that name.\n#[test]\nfn enemy_is_reachable_through_engine() {\nlet enemy = crate::engine::Enemy::for_stage(4);\nassert_eq!(enemy.health, 50);\nassert_eq!(enemy.name, \"Training Slime\");\n}\n/// Calling `main()` should work once the door is open. Every import in the file has to resolve.\n#[test]\nfn main_runs() {\nmain();\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/).\nTip:\nInside engine, the enemy module is right beside you, so the path is short: pub use enemy::Enemy;.\nBuild again.\nNow the remaining errors are about rules:\nerror[E0425]: cannot find function `xp_to_next_level` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\nerror[E0425]: cannot find function `click_damage` in this scope\nThose calls used to work because main.rs imported the rules directly.\nNow rules is private inside the library.\nThere are two ways we could fix that.\nWe could put rules on the front door too:\npub mod rules;\nThen main.rs could import those formulas and keep doing this:\nclick_damage(\ngame.player.base_damage(),\ngame.player.bonus_damage()\n)\nThat would compile.\nBut think about what it means.\nEvery formula in the game becomes part of the library's public API.\nAnd the screen keeps doing game arithmetic.\nToday the screen is a terminal.\nLater it will be a browser.\nIf both of them know how to calculate damage, we now have multiple callers depending on an internal game rule.\nThe rules should live in one place.\nThe screen should ask for answers.\nSo we take the second path.\nLeave rules private.\nLet Player provide the information the outside world needs.\nAdd two methods inside impl Player, after add_xp():\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(\nself.base_damage(),\nself.bonus_damage()\n)\n}\nThe method and the rule share a name.\nThat is fine.\nInside the method:\nclick_damage(...)\nwith no self. in front of it means the imported rule.\nThe method is the player asking that rule a question about itself.\nFirst, add click_damage to the use crate::rules::{...} list at the top of player.rs.\nIt was not there before because Player used to leave that arithmetic to attack().\nWhile we are in there, add_xp() has been calling:\nxp_to_next_level(self.level)\ntwice.\nNow that the player has a method for that question, replace its loop with:\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\nThe loop is still the same loop.\nxp_to_next_level stays imported because xp_needed() uses it.\nattack() in game.rs can lean on the other new method too:\nfn attack(player: &Player, enemy: &mut Enemy) {\nenemy.health =\nhealth_after_hit(\nenemy.health,\nplayer.click_damage()\n);\n}\nIt no longer needs the click_damage rule, so remove that name from the use statement at the top of game.rs.\nLast, main() gets simpler.\nWhere it said:\nxp_to_next_level(game.player.level)\nit now says:\ngame.player.xp_needed()\nAnd where it said:\nclick_damage(\ngame.player.base_damage(),\ngame.player.bonus_damage()\n)\nit now says:\ngame.player.click_damage()\nThat second replacement happens twice in main().\nBuild, test, and run:\ncargo test\ncargo run\nSame twenty-one tests.\nSame output.\nCargo also prints test blocks for main.rs and doc-tests that say:\nrunning 0 tests\nThat is normal.\nAnd main.rs no longer knows a single game formula.\nMake the front door smaller\nWe said everything was too pub.\nTime to fix that.\nRight now main() can see far more than it needs.\nTry it.\nAdd this line just after:\nlet mut game = Game::new();\ngame.player.add_gold(1_000_000);\nIt compiles.\nA screen can hand the player a million gold, and the game never gets a say.\nLet us close that off.\nRust has a visibility setting between fully private and fully public:\npub(crate)\nIt means:\nVisible anywhere inside this crate, but not outside it.\nOur library can use the item freely.\nmain.rs belongs to a different crate now, so it cannot.\nGo through the files and tighten everything main() does not need.\nThe rule for now is:\npub for what outside code needs. pub(crate) or private for everything else.\nIn rules.rs, change all eleven:\npub fn\nto:\npub(crate) fn\nIn enemy.rs:\nchange for_stage() to pub(crate)\nmake gold_reward and xp_reward pub(crate)\nleave name and health public because main() displays them\nIn player.rs, change:\nnew\nadd_gold\nadd_xp\nbuy_click_upgrade\nto pub(crate).\nTake pub off base_damage() and bonus_damage() entirely.\nOnly Player needs those now.\nWhat remains fully public is what the current screen needs:\nGame\nnew\nclick\ntick\nbuy_click_upgrade\nstage\nplayer\nenemy\nPlayer\ngold\nxp\nlevel\nclick_upgrade_level\nxp_needed\nclick_damage\nEnemy\nname\nhealth\nPurchaseOutcome\nNow try this again:\ngame.player.add_gold(1_000_000);\nIt fails.\nadd_gold() is pub(crate), and main.rs is outside the library crate.\nDelete that line.\nRun:\ncargo test\ncargo run\nNothing changes.\nThat is the point.\nIf tightening the boundary had broken main(), it would mean main() depended on something we did not actually want to promise publicly.\nNow try to cheat another way.\nIn main(), add:\nlet secret = game.enemy.gold_reward;\nRust says no:\nerror[E0616]: field `gold_reward` of struct `Enemy` is private\nDelete it.\nNow try reaching for an internal damage detail.\nReplace:\ngame.player.click_damage()\nwith:\ngame.player.base_damage()\nRust refuses again:\nerror[E0624]: method `base_damage` is private\nPut it back.\nThose errors are the wall doing its job.\nTry it: here is the same idea in one file. base_damage is private to the player module, and click_damage is the public question the outside world is allowed to ask. Run it and you should see Click damage: 7. Then remove the // in front of the last println! in main and run again. You should get E0624, method base_damage is private, which is exactly the error above.\nLive example · reach for the private method\nmod player {\npub struct Player {\npub level: u32,\n}\nimpl Player {\nfn base_damage(&self) -> u32 {\nself.level + 4\n}\npub fn click_damage(&self) -> u32 {\nself.base_damage()\n}\n}\n}\nuse player::Player;\nfn main() {\nlet player = Player { level: 3 };\nprintln!(\"Click damage: {}\", player.click_damage());\n// println!(\"Base damage: {}\", player.base_damage());\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/).\nQuestion: Once add_gold() is pub(crate), why can main() no longer call it?\npub(crate) is the same as private, so only Player itself can call it\nmain.rs is now a different crate from the library, and pub(crate) only reaches code inside the same crate\nMethods can never be called on a field like game.player\nAnswer: 2. main.rs is now a different crate from the library, and pub(crate) only reaches code inside the same crate\nExplanation: The rest of the library can still call add_gold(). Only code outside the crate, like our binary, is shut out.\nA test from the outside\nOur twenty-one tests all live inside the library, next to the code they check.\nThat is useful.\nThose tests can work closely with the modules they belong to.\nBut it leaves another question:\nDoes the library work for code that only gets to use the public front door?\nCargo has a place for that.\nCreate a folder called tests next to src, and put a file in it:\nrustbound-idle/\n├── src/\n└── tests/\n└── idle_hour.rs\nEach Rust file directly inside tests/ is compiled as a separate integration test crate.\nIt uses the library the same way outside code does: through the public API.\nThis is the first code in the project that sees the game the way a stranger would.\nPut this in tests/idle_hour.rs:\nuse rustbound_idle::Game;\n#[test]\nfn an_hour_away_moves_the_game_forward() {\nlet mut game = Game::new();\ngame.tick(3600);\nassert_eq!(game.stage, 128);\nassert_eq!(game.player.level, 47);\n}\nThe terminal program clicked through three stages before it waited an hour.\nThis test waits an hour from a brand-new game, so the numbers are different:\nstage 128\nlevel 47\nRun everything:\ncargo test\nCargo prints a block for each piece it tests.\nYou should see:\nthe twenty-one tests inside the library\nthe new integration test in tests/idle_hour.rs\nblocks for main.rs and doc-tests that say running 0 tests\nThat is fine.\nTwenty-two tests in all.\nQuestion: Why can tests/idle_hour.rs not call a pub(crate) function from the library?\nIntegration tests cannot call any functions at all\nEach file in tests/ is its own crate and sees only the public API, like any outside code\npub(crate) items are removed when tests are compiled\nAnswer: 2. Each file in tests/ is its own crate and sees only the public API, like any outside code\nExplanation: That is what makes it a test from the outside: if it compiles there, a stranger could write the same code.\nOne last complaint\nThere is one more thing, and it has been waiting for us.\nClippy is Rust's linter.\nIt looks for patterns that compile but could be clearer, more idiomatic, or less error-prone.\nIf:\ncargo clippy\nis not available, install the component first:\nrustup component add clippy\nThen run:\ncargo clippy\nClippy has a suggestion:\nwarning: you should consider adding a `Default` implementation for `Game`\nIt did not complain before because Game::new() was not part of a public API.\nNow Game is public, and so is its zero-argument new().\nClippy notices that callers may reasonably expect the standard Rust Default trait too.\nDefault describes a type's standard starting value.\nWe already have one:\nGame::new()\nSo we can say that explicitly:\nimpl Default for Game {\nfn default() -> Game {\nGame::new()\n}\n}\nPut it in game.rs, between the impl Game block and attack().\nNow:\nGame::new()\nand:\nGame::default()\nproduce the same starting game.\nRun Clippy again.\nIt should be quiet.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along in your editor, here is everything in one place.\nYour project should now look like this:\nrustbound-idle/\n├── Cargo.toml\n├── src/\n│   ├── lib.rs\n│   ├── main.rs\n│   ├── rules.rs\n│   ├── enemy.rs\n│   ├── player.rs\n│   └── game.rs\n└── tests/\n└── idle_hour.rs\n<details>\n<summary><code>src/lib.rs</code> · 8 lines</summary>\nmod enemy;\nmod game;\nmod player;\nmod rules;\npub use enemy::Enemy;\npub use game::Game;\npub use player::{Player, PurchaseOutcome};\n</details>\n<details>\n<summary><code>src/main.rs</code> · 62 lines</summary>\nuse rustbound_idle::{Game, PurchaseOutcome};\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\ngame.player.xp_needed()\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\ngame.player.click_damage()\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name,\ngame.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\ngame.player.click_damage(),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage,\ngame.stage,\nlevel,\ngame.player.level,\ngold,\ngame.player.gold\n);\n}\n</details>\n<details>\n<summary><code>src/rules.rs</code> · 107 lines</summary>\npub(crate) fn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\npub(crate) fn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\npub(crate) fn is_defeated(health: u32) -> bool {\nhealth == 0\n}\npub(crate) fn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\npub(crate) fn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\npub(crate) fn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\npub(crate) fn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\npub(crate) fn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\npub(crate) fn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\npub(crate) fn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\npub(crate) fn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades\n.saturating_add(1)\n.saturating_mul(10)\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n}\n</details>\n<details>\n<summary><code>src/enemy.rs</code> · 23 lines</summary>\nuse crate::rules::{\nenemy_health_for_stage,\ngold_reward_for_stage,\nxp_reward_for_stage,\n};\npub struct Enemy {\npub name: String,\npub health: u32,\npub(crate) gold_reward: u32,\npub(crate) xp_reward: u32,\n}\nimpl Enemy {\npub(crate) fn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\n</details>\n<details>\n<summary><code>src/player.rs</code> · 129 lines</summary>\nuse crate::rules::{\nbase_damage_for_level,\nclick_damage,\nclick_upgrade_bonus,\nclick_upgrade_cost,\nxp_to_next_level,\n};\npub struct Player {\npub gold: u32,\npub xp: u32,\npub level: u32,\npub click_upgrade_level: u32,\n}\n#[derive(Debug, PartialEq)]\npub enum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\npub(crate) fn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\npub(crate) fn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\npub(crate) fn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= self.xp_needed() {\nself.xp -= self.xp_needed();\nself.level = self.level.saturating_add(1);\n}\n}\npub fn xp_needed(&self) -> u32 {\nxp_to_next_level(self.level)\n}\npub fn click_damage(&self) -> u32 {\nclick_damage(\nself.base_damage(),\nself.bonus_damage()\n)\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\npub(crate) fn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost =\nclick_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level =\nself.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::NotEnoughGold\n);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(\nplayer.buy_click_upgrade(),\nPurchaseOutcome::Purchased\n);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n}\n</details>\n<details>\n<summary><code>src/game.rs</code> · 186 lines</summary>\nuse crate::enemy::Enemy;\nuse crate::player::{Player, PurchaseOutcome};\nuse crate::rules::{\nhealth_after_hit,\nidle_damage_for_level,\nis_defeated,\n};\npub struct Game {\npub stage: u32,\npub player: Player,\npub enemy: Enemy,\n}\nimpl Game {\npub fn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\npub fn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\npub fn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage =\nidle_damage_for_level(self.player.level);\nself.enemy.health =\nhealth_after_hit(\nself.enemy.health,\ndamage\n);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\npub fn buy_click_upgrade(\n&mut self\n) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nimpl Default for Game {\nfn default() -> Game {\nGame::new()\n}\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nenemy.health =\nhealth_after_hit(\nenemy.health,\nplayer.click_damage()\n);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(\nlong.enemy.health,\nshort.enemy.health\n);\nassert_eq!(\nlong.player.gold,\nshort.player.gold\n);\nassert_eq!(\nlong.player.xp,\nshort.player.xp\n);\nassert_eq!(\nlong.player.level,\nshort.player.level\n);\n}\n}\n</details>\n<details>\n<summary><code>tests/idle_hour.rs</code> · 11 lines</summary>\nuse rustbound_idle::Game;\n#[test]\nfn an_hour_away_moves_the_game_forward() {\nlet mut game = Game::new();\ngame.tick(3600);\nassert_eq!(game.stage, 128);\nassert_eq!(game.player.level, 47);\n}\n</details>\nRun everything:\ncargo run\ncargo test\ncargo clippy\nThe game should clear three stages, buy one upgrade, and spend an hour away.\nAll twenty-two tests should pass.\nClippy should be quiet.\nWhat we learned\nAlong the way, we encountered:\nmodules, and mod to declare one\norganizing top-level modules into separate files\nuse, and crate:: paths\nRust's default privacy boundaries\npub\npub(crate)\npub use, for choosing what appears on a library's front door\na library crate and a binary crate in the same Cargo package\ntests that live beside their code\nintegration tests in tests/\nDefault, which Clippy asked us to provide\nTwo ideas are worth keeping.\nThe first is that privacy is not about hiding.\nIt is about deciding what everyone else is allowed to depend on.\nA private function gives us freedom.\nWe can rename it, reorganize it, or replace its implementation without outside code depending on that detail, as long as the public behavior stays the same.\nresolve_hit() can move.\nbase_damage() can change.\nCode outside their visibility boundary does not get to build itself around those details.\nQuestion: What does keeping resolve_hit() private buy us?\nIt runs faster than a public function\nWe can change or move it later without breaking any code outside its module\nIt stops the compiler from warning about unused code\nAnswer: 2. We can change or move it later without breaking any code outside its module\nExplanation: A public item is a promise to everyone who uses it. A private one is a detail we are free to change.\nThe second idea is where the line went.\nWe did not decide to build an engine.\nWe sorted the code, and the sorting kept asking one question:\nDoes this need to know about a screen?\nFor everything except main(), the answer was no.\nThat is the heart of what our little game engine is becoming.\nIt is not a graphics library.\nIt does not open a window.\nIt does not print to a terminal.\nIt does not know whether a browser even exists.\nIt owns rules and state.\nThe game's four main commands are:\nnew\nclick\ntick\nbuy_click_upgrade\nWe still expose some state so main() can display it.\nThat part of the front door is wider than I want.\nGame, Player, and Enemy still have public fields, which means main() could write:\ngame.enemy.health = 0;\nand the compiler would allow it.\nThat is a loose end.\nWe are going to lean on it soon.\nIt cannot remember\nOur game can simulate an hour instantly.\nIt cannot survive the program ending.\nClose the terminal and everything is gone:\nstage\ngold\nXP\nlevel\nupgrades\nAn idle game that forgets you the moment you leave is not much of an idle game.\nNext time, the game learns to save itself.\nNot to a file.\nNot to a browser.\nNot to a server.\nThe library will not know where saves go, and it should not.\nIt will only know how to say, in a form that can be written down:\nThis is everything about me.\nAnd later, given that same form:\nI remember.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-modules-library-crate-split-game/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-25T15:09:41Z",
      "id": "https://mnixblog.netlify.app/rust-modules-library-crate-split-game/",
      "image": "https://mnixblog.netlify.app/images/rust-modules-library-crate-split-game/cover.jpg",
      "summary": "Split a Rust game into modules and a library crate, step by step, with tests passing after every move. Learn mod, pub, and pub use.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "We Accidentally Built a Game Engine.",
      "url": "https://mnixblog.netlify.app/rust-modules-library-crate-split-game/"
    },
    {
      "content_html": "<p>Our game has a progression loop now.</p>\n<p>Kill a slime, get paid, level up, buy damage, kill a tougher slime.</p>\n<p>It still stops the moment you stop clicking.</p>\n<p>So today we are going to make time part of the game.</p>\n<p>Not a timer. Not a clock. Not <code>sleep</code>.</p>\n<p>Just a number.</p>\n<p>Because the game will never look at a clock, we can test a full hour away in a fraction of a second.</p>\n<p>By the end, an hour away from the game is one line:</p>\n<pre><code class=\"language-rust\">game.tick(3600);\n</code></pre>\n<p>And here is what the game looks like when you come back:</p>\n<pre><code class=\"language-text\">One hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, you can skip past this code to \"Time is just a number.\"</p>\n<p>If you are building along, you need three structs, an enum, three <code>impl</code> blocks, eleven small functions, and sixteen tests:</p>\n<details>\n<summary><code>src/main.rs</code> · 339-line starting point</summary>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n</details>\n<p>Run it, and the player clears eight stages and buys three upgrades.</p>\n<p>Then the program is over, and so is all the progress, because nothing in it ever happens on its own.</p>\n<hr />\n<h2 id=\"time-is-just-a-number\">Time is just a number<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#time-is-just-a-number\" aria-label=\"Link to this section\">#</a></h2>\n<p>An idle game has to keep going while nobody is looking.</p>\n<p>The obvious way to build that is to give the game a clock.</p>\n<p>Start a timer. Every second, do some work. When the player leaves, keep the timer running.</p>\n<p>That sounds simple, and it is a trap.</p>\n<p>A game that watches a clock is a game that has to be tested by waiting, unless you build a fake clock, and then you have rebuilt the idea we are about to use anyway.</p>\n<p>Want to know what happens after an hour away? Wait an hour.</p>\n<p>So let's turn it around.</p>\n<p>The game will not ask what time it is.</p>\n<p>Whoever is running the game will tell it how much time went by:</p>\n<pre><code class=\"language-rust\">game.tick(10);\n</code></pre>\n<p>Ten seconds passed. What happened?</p>\n<p>Game code often uses the word <em>tick</em> for advancing simulation time, so we will borrow the name.</p>\n<p>The contract is one sentence:</p>\n<blockquote>\n<p>Tell me how many seconds passed. I will tell you what the game became.</p>\n</blockquote>\n<p><code>tick</code> will be a method on <code>Game</code>, and the only thing it takes is that number.</p>\n<p>The number is the only thing about time that Rust ever sees.</p>\n<p>Later in this series, a web page will measure real time and hand the game that number.</p>\n<p>For now, our tests can hand it any number they like.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>tick(seconds)</code> take a number instead of reading a clock?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A test can hand it an hour of time and finish instantly</li>\n<li>Rust has no way to read a clock at all</li>\n<li>A <code>u32</code> automatically counts real seconds</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. A test can hand it an hour of time and finish instantly</p>\n<p>The game only sees a number, so whoever calls it decides how much time went by. A test can say <code>3600</code> without waiting.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"what-does-a-player-do-while-nobody-is-clicking\">What does a player do while nobody is clicking?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#what-does-a-player-do-while-nobody-is-clicking\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before the game can work out what happened, it needs to know what the player does with spare seconds.</p>\n<p>Our player is going to keep fighting.</p>\n<p>One hit per second, and the harder the player has leveled up, the harder it hits.</p>\n<p>That is a game rule, and we know what to do with game rules by now.</p>\n<p>Give it a small function, next to the others:</p>\n<pre><code class=\"language-rust\">fn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n</code></pre>\n<p>One damage per second at level 1.</p>\n<p>Two at level 2.</p>\n<p>It is deliberately small. It is not balanced and it does not need to be yet.</p>\n<p>It also ignores the click upgrades on purpose. Those are for clicking.</p>\n<p>What matters is that the rule has a name and a test.</p>\n<p>Add it at the bottom of the <code>tests</code> module:</p>\n<pre><code class=\"language-rust\">#[test]\nfn idle_damage_grows_with_level() {\n    assert_eq!(idle_damage_for_level(1), 1);\n    assert_eq!(idle_damage_for_level(2), 2);\n}\n</code></pre>\n<p>Notice what the function does not take.</p>\n<p>It does not take a <code>Player</code>. It does not take a <code>Game</code>.</p>\n<p>It takes a level and returns a number. It is the same kind of function as <code>base_damage_for_level()</code>, and it asks the same question: what does this level of progress get you?</p>\n<p>If you run <code>cargo build</code> now, Rust warns that <code>idle_damage_for_level</code> is never used.</p>\n<p>We are about to use it.</p>\n<hr />\n<h2 id=\"one-second-at-a-time\">One second at a time<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#one-second-at-a-time\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now for <code>tick()</code>.</p>\n<p>The player hits once per second, so ten seconds is ten hits.</p>\n<p>We know exactly how many times to repeat: once per second that passed.</p>\n<p>That is not a job for <code>while</code>.</p>\n<p>A <code>while</code> loop keeps going until a condition stops being true, like our level-up loop, which cannot know how many levels one reward will cross.</p>\n<p>When you know the count in advance, Rust has a loop for that:</p>\n<pre><code class=\"language-rust\">for _ in 0..seconds {\n    // one second of work\n}\n</code></pre>\n<p><code>0..seconds</code> is a range.</p>\n<p>It counts from 0 up to, but not including, <code>seconds</code>.</p>\n<p>So <code>0..3</code> gives you 0, 1, and 2. That is three trips through the loop, which is exactly what we want for three seconds.</p>\n<p>Try it: run this, then change <code>seconds</code> to <code>0</code> and to <code>5</code>, and count the trips.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the seconds away</figcaption><pre><code class=\"language-rust\">fn main() {\n    let seconds: u32 = 3;\n    let mut trips = 0;\n\n    for second in 0..seconds {\n        println!(&quot;trip with range value {second}&quot;);\n        trips += 1;\n    }\n\n    println!(&quot;{seconds} seconds away, {trips} trips through the loop&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/\">the original article</a>.</p></figure>\n<p>With <code>3</code> you should see range values <code>0</code>, <code>1</code> and <code>2</code>, then <code>3 seconds away, 3 trips through the loop</code>. With <code>0</code> the loop never runs, and the last line says <code>0 trips</code>.</p>\n<p>The <code>_</code> is a placeholder that means:</p>\n<blockquote>\n<p>I do not need this value.</p>\n</blockquote>\n<p>A <code>for</code> loop hands us the current number from the range on every trip.</p>\n<p>We do not care which second we are on. We only care that one passed.</p>\n<p>If we gave the number a name like <code>second</code>, Rust would warn that we never use it.</p>\n<p>The <code>_</code> says that is on purpose.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why is <code>tick()</code> a <code>for</code> loop instead of a <code>while</code> loop?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It is given the number of seconds, so the count is known in advance</li>\n<li>A <code>while</code> loop can only run once</li>\n<li>A <code>for</code> loop skips the enemy health check</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It is given the number of seconds, so the count is known in advance</p>\n<p>A <code>while</code> loop fits when you cannot know how many trips you need, like the level-up loop. Here the caller hands us the count.</p>\n</div>\n</div>\n</section>\n<p>Write the first version inside the <code>impl Game</code> block, right under <code>click()</code>. It is indented because it lives inside that block:</p>\n<pre><code class=\"language-rust\">    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n        }\n    }\n</code></pre>\n<p>Each trip through the loop works out the damage for the player's current level and hits the enemy with it.</p>\n<p>It uses <code>health_after_hit()</code>, the same small rule that has been protecting enemy health since the first article.</p>\n<p>That looks finished.</p>\n<p>It is not, and we should have a test to say so.</p>\n<hr />\n<h2 id=\"the-slime-that-died-and-kept-being-hit\">The slime that died and kept being hit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#the-slime-that-died-and-kept-being-hit\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have a rule for how fast the player hits.</p>\n<p>What we need next is a test for the thing a player cares about: if I wait long enough, does the slime go down?</p>\n<p>At level 1, our player hits for 1 per second, and the first slime has 20 health.</p>\n<p>Twenty seconds should do it:</p>\n<pre><code class=\"language-rust\">#[test]\nfn enough_time_defeats_the_enemy() {\n    let mut game = Game::new();\n\n    game.tick(20);\n\n    assert_eq!(game.stage, 2);\n    assert_eq!(game.enemy.health, 30);\n    assert_eq!(game.player.gold, 5);\n    assert_eq!(game.player.xp, 6);\n}\n</code></pre>\n<p>Those last numbers should look familiar.</p>\n<p>They are the same ones the click version of this test checks: stage 2, a 30 health enemy, 5 gold, 6 XP.</p>\n<p>It does not matter whether a slime dies from a click or from a wait.</p>\n<p>The player should get paid the same either way.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo test enough_time\n</code></pre>\n<p>and it fails:</p>\n<pre><code class=\"language-text\">assertion `left == right` failed\n  left: 1\n right: 2\n</code></pre>\n<p>(Rust prints more than that, including a line number and a note about backtraces. This is the part that matters.)</p>\n<p><code>left</code> is what the game did. <code>right</code> is what the test expected.</p>\n<p>The stage is still 1.</p>\n<p>Twenty seconds passed. The slime's health reached zero on the twentieth hit.</p>\n<p>And then nothing happened.</p>\n<p>Our loop never asks whether the enemy is defeated.</p>\n<p>That check lives in <code>click()</code>:</p>\n<pre><code class=\"language-rust\">        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n</code></pre>\n<p><code>tick()</code> took the hit from <code>click()</code> but left the aftermath behind.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why was the stage still 1 after the first version of <code>tick(20)</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The slime's health reached zero, but <code>tick()</code> never checked <code>is_defeated</code></li>\n<li><code>0..20</code> only runs 19 times, so the last hit never landed</li>\n<li><code>saturating_sub</code> wrapped around below zero</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. The slime's health reached zero, but <code>tick()</code> never checked <code>is_defeated</code></p>\n<p>The loop landed all twenty hits. Only <code>click()</code> knew what to do afterward, so <code>tick()</code> needed the same check.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"two-ways-to-land-a-hit\">Two ways to land a hit<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#two-ways-to-land-a-hit\" aria-label=\"Link to this section\">#</a></h2>\n<p>There are two ways to hurt an enemy now: a click, and a second of waiting.</p>\n<p>Both of them need the same thing to happen afterward.</p>\n<p>If we paste the <code>if</code> into <code>tick()</code>, we have the same three lines in two places.</p>\n<p>Then the next time defeating an enemy needs to change, we have to remember both.</p>\n<p>We have seen this before.</p>\n<p>When the same enemy was being built in three different places, we gave the building a name, and <code>Enemy::for_stage()</code> became the one place that builds an enemy.</p>\n<p>Same fix here.</p>\n<p>Add a method inside the same <code>impl Game</code> block, under <code>tick()</code>:</p>\n<pre><code class=\"language-rust\">    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n</code></pre>\n<p>Read it out loud:</p>\n<blockquote>\n<p>A hit just landed. If the enemy went down, handle the defeat.</p>\n</blockquote>\n<p>Now both ways of landing a hit can finish the same way.</p>\n<p>Replace the whole <code>click()</code> method with this one. The old <code>if</code> is gone, and a call to <code>resolve_hit()</code> takes its place:</p>\n<pre><code class=\"language-rust\">    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n</code></pre>\n<p>And replace <code>tick()</code> with this. Only the last line inside the loop is new:</p>\n<pre><code class=\"language-rust\">    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n</code></pre>\n<p>Run the tests again:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All eighteen pass, including the clicking tests we did not touch.</p>\n<p>That is what a good extraction looks like.</p>\n<p><code>click()</code> does the same thing it did before.</p>\n<p>It just does it by calling a method that <code>tick()</code> can call too.</p>\n<p>Now follow the path of the twentieth second:</p>\n<pre><code class=\"language-text\">tick()\n    ↓\nhealth_after_hit() → 0\n    ↓\nresolve_hit()\n    ↓\ndefeat_enemy()\n    ↓\ngold, XP, next stage, next enemy\n</code></pre>\n<p>That bottom half is the same code a click reaches.</p>\n<p>The slime can die from a click or from a second of waiting.</p>\n<p>Either way, it dies the same way.</p>\n<hr />\n<h2 id=\"test-time-without-waiting\">Test time without waiting<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#test-time-without-waiting\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at what we just tested.</p>\n<p>Twenty seconds of game time ran, and the test finished instantly.</p>\n<p>Nothing slept. Nothing waited.</p>\n<p>The game does not know what a second is.</p>\n<p>It was handed the number 20 and did twenty steps of work.</p>\n<p>That is the payoff for the design.</p>\n<p>Time is an input, so a test can supply as much of it as it likes.</p>\n<p>Three more tests are worth adding. Put each one at the bottom of the <code>tests</code> module.</p>\n<p>One second should be one hit:</p>\n<pre><code class=\"language-rust\">#[test]\nfn one_second_is_one_idle_hit() {\n    let mut game = Game::new();\n\n    game.tick(1);\n\n    assert_eq!(game.enemy.health, 19);\n}\n</code></pre>\n<p>Zero seconds should change nothing:</p>\n<pre><code class=\"language-rust\">#[test]\nfn zero_seconds_changes_nothing() {\n    let mut game = Game::new();\n\n    game.tick(0);\n\n    assert_eq!(game.stage, 1);\n    assert_eq!(game.enemy.health, 20);\n}\n</code></pre>\n<p>We did not write any code to handle zero.</p>\n<p>We did not need to.</p>\n<p><code>0..0</code> is an empty range, so the loop runs zero times and the game stays exactly as it was.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>game.tick(0)</code> leave the game unchanged without any special code for zero?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>0..0</code> is an empty range, so the loop body never runs</li>\n<li><code>health_after_hit</code> refuses a damage of zero</li>\n<li>Rust skips any method called with <code>0</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>0..0</code> is an empty range, so the loop body never runs</p>\n<p>No trips through the loop means no damage, no defeat, and no payout.</p>\n</div>\n</div>\n</section>\n<p>The last test is the one that matters most.</p>\n<p>Waiting forty-five seconds in one go should give the same game as waiting one second forty-five times:</p>\n<pre><code class=\"language-rust\">#[test]\nfn one_long_tick_matches_many_short_ones() {\n    let mut long = Game::new();\n    let mut short = Game::new();\n\n    long.tick(45);\n\n    for _ in 0..45 {\n        short.tick(1);\n    }\n\n    assert_eq!(long.stage, short.stage);\n    assert_eq!(long.enemy.health, short.enemy.health);\n    assert_eq!(long.player.gold, short.player.gold);\n    assert_eq!(long.player.xp, short.player.xp);\n    assert_eq!(long.player.level, short.player.level);\n}\n</code></pre>\n<p>This one passes by construction today, because <code>tick(45)</code> literally is forty-five single steps.</p>\n<p>Today it almost looks redundant. Later, when we replace thousands of tiny steps with arithmetic, it becomes the test that proves the optimization did not change the game.</p>\n<p>It is a promise to our future selves.</p>\n<p>Later, a browser will sometimes call <code>tick(1)</code> every second.</p>\n<p>Other times, it will come back after a long absence and call <code>tick(7200)</code> once.</p>\n<p>Those two have to be the same game.</p>\n<p>If a change ever makes them different, this test will say so.</p>\n<p>Try it: here is a trimmed copy of our game with just <code>tick()</code>, <code>resolve_hit()</code> and the defeat rules. Change <code>seconds</code> and run it. Then try <code>3600</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the seconds away</figcaption><pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n\nstruct Enemy {\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player { gold: 0, xp: 0, level: 1 }\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            health: stage.saturating_mul(10).saturating_add(10),\n            gold_reward: stage.saturating_mul(2).saturating_add(3),\n            xp_reward: stage.saturating_mul(2).saturating_add(4),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game { stage: 1, player: Player::new(), enemy: Enemy::for_stage(1) }\n    }\n\n    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = self.enemy.health.saturating_sub(damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if self.enemy.health == 0 {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.gold = self.player.gold.saturating_add(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level.saturating_sub(1).saturating_mul(15).saturating_add(20)\n}\n\nfn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\nfn main() {\n    let seconds = 45;\n\n    let mut long = Game::new();\n    let mut short = Game::new();\n\n    long.tick(seconds);\n\n    for _ in 0..seconds {\n        short.tick(1);\n    }\n\n    println!(\n        &quot;One long tick:   stage {} health {} gold {} xp {} level {}&quot;,\n        long.stage, long.enemy.health, long.player.gold, long.player.xp, long.player.level\n    );\n    println!(\n        &quot;Many short ones: stage {} health {} gold {} xp {} level {}&quot;,\n        short.stage, short.enemy.health, short.player.gold, short.player.xp, short.player.level\n    );\n\n    assert_eq!(long.stage, short.stage);\n    assert_eq!(long.enemy.health, short.enemy.health);\n    assert_eq!(long.player.gold, short.player.gold);\n    assert_eq!(long.player.xp, short.player.xp);\n    assert_eq!(long.player.level, short.player.level);\n\n    println!(&quot;Same game.&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/\">the original article</a>.</p></figure>\n<p>With <code>45</code> it prints the same line twice, <code>stage 2 health 5 gold 5 xp 6 level 1</code>, then <code>Same game.</code> The <code>assert_eq!</code> lines in <code>main</code> are our test; if the two games ever differ, the program stops with a panic instead.</p>\n<p>Run them all:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>Twenty-one tests should pass.</p>\n<hr />\n<h2 id=\"leave-for-an-hour\">Leave for an hour<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#leave-for-an-hour\" aria-label=\"Link to this section\">#</a></h2>\n<p>Time to try it for real.</p>\n<p>We are going to leave the loop in <code>main()</code> exactly as it is, with one change: stop at stage 3, so the output is short.</p>\n<pre><code class=\"language-rust\">    while game.stage &lt;= 3 {\n</code></pre>\n<p>Everything inside the loop stays the same.</p>\n<p>Then, after the loop, the player leaves.</p>\n<p>Add this after the closing brace of the <code>while</code> loop, still inside <code>main()</code>:</p>\n<pre><code class=\"language-rust\">    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n</code></pre>\n<p>Three thousand six hundred is the number of seconds in an hour.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nOne hour away.\nStage 4 -&gt; 131  Level 2 -&gt; 48  Gold 11 -&gt; 17410\n</code></pre>\n<p>The first half is the player, clicking their way through three stages.</p>\n<p>The last two lines are the hour.</p>\n<p>One hour, with nobody at the keyboard, took the game from stage 4 to stage 131.</p>\n<p>The player also came home with 17,410 gold and nothing spent. Noted.</p>\n<p>Look at the level.</p>\n<p>It went from 2 to 48.</p>\n<p>That was not a bug.</p>\n<p>Every trip through the <code>for</code> loop asks for the damage at the player's <em>current</em> level.</p>\n<p>When a slime pays out enough XP, the player levels up in the middle of the hour, and every second after that hits a little harder.</p>\n<p>Waiting makes the player stronger, which makes waiting faster.</p>\n<p>That is the shape of an idle game.</p>\n<p>Is it balanced? No.</p>\n<p>A stage 131 slime after an hour is a lot.</p>\n<p>We will tune that later. We have a rule with a name and a test, so tuning it is changing one function.</p>\n<p><strong>Coding challenge: Tune the idle hit</strong></p>\n<p>Change <code>idle_damage_for_level</code> so that level 1 still hits for 1, but every level after that adds 2 more damage per second.</p>\n<p>Keep the function named <code>idle_damage_for_level</code>, taking a <code>u32</code> level and returning a <code>u32</code>, because the checks call it. <strong>Check my code</strong> will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> the program prints <code>Level 1: 1 damage per second</code>, <code>Level 2: 3 damage per second</code> and <code>Level 3: 5 damage per second</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Finish the idle damage rule</figcaption><pre><code class=\"language-rust\">fn idle_damage_for_level(level: u32) -&gt; u32 {\n    // TODO: level 1 hits for 1, and every level after that adds 2 more\n    level\n}\n\nfn main() {\n    for level in 1..=3 {\n        println!(&quot;Level {level}: {} damage per second&quot;, idle_damage_for_level(level));\n    }\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// Level 1 is the base hit and gets nothing extra. Look at what your rule does with the first level.\n#[test]\nfn level_one_is_the_base_hit() {\n    assert_eq!(idle_damage_for_level(1), 1);\n}\n\n/// Levels 2 and 3 should each step up from the level before. Check the size of the step.\n#[test]\nfn each_level_adds_two() {\n    assert_eq!(idle_damage_for_level(2), 3);\n    assert_eq!(idle_damage_for_level(3), 5);\n}\n\n/// The step should stay the same as levels climb. Try your rule on a level further out.\n#[test]\nfn later_levels_keep_the_step() {\n    assert_eq!(idle_damage_for_level(10), 19);\n    assert_eq!(idle_damage_for_level(100), 199);\n}\n\n/// Level 0 should not crash the game. Unsigned numbers do not like going below zero.\n#[test]\nfn level_zero_does_not_panic() {\n    let _ = idle_damage_for_level(0);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Start from <code>level.saturating_sub(1)</code>, like <code>xp_to_next_level</code> does, so level 1 gets no extra. Multiply that by 2 and add the 1 back.</p>\n</aside>\n<p>What matters today is that the engine can answer a question it could not answer before:</p>\n<blockquote>\n<p>What happened while I was away?</p>\n</blockquote>\n<hr />\n<h2 id=\"who-owns-the-clock\">Who owns the clock?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#who-owns-the-clock\" aria-label=\"Link to this section\">#</a></h2>\n<p>The game never asked what time it was.</p>\n<p>It was handed 3600, and it did 3600 seconds of work.</p>\n<p>That is going to become one of the most important lines in this series:</p>\n<pre><code class=\"language-text\">Rust owns the rules.\nWhatever runs the game owns time.\n</code></pre>\n<p>Today, that caller is a test or <code>main()</code>. Later, it will be a web page.</p>\n<p>The caller measures how much real time passed and hands that fact to the game.</p>\n<p>Rust decides what that time means for gameplay.</p>\n<p>That distinction matters. A browser might report that 28,800 seconds passed while the player was away. The game can still decide that only part of that time should count.</p>\n<p>In other words:</p>\n<pre><code class=\"language-text\">caller measures elapsed time\n        ↓\nRust receives the number\n        ↓\nRust decides how much time counts\n        ↓\nRust applies the game rules\n</code></pre>\n<p>That keeps balance decisions inside the game instead of hiding them in whichever environment happens to run it.</p>\n<p>There is one honest cost.</p>\n<p><code>tick()</code> does one trip through the loop for every second.</p>\n<p>An hour is 3,600 trips. A day is 86,400.</p>\n<p>That is still manageable for this tiny program, but the cost grows directly with the number of seconds we simulate.</p>\n<p>A number like <code>u32::MAX</code> would be a very long walk.</p>\n<p>Later, we can cap credited away-time or replace repeated steps with arithmetic. The caller will still report elapsed time; Rust will still own the policy and the rules.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A browser reports 28,800 seconds away. Why should the game, not the caller, decide how many of them count?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Balance rules belong in Rust, so every caller gets the same policy</li>\n<li>The caller cannot measure elapsed time</li>\n<li><code>tick</code> rejects any number above 3600</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Balance rules belong in Rust, so every caller gets the same policy</p>\n<p>The caller measures; Rust decides what that time means. Capping away-time is a game rule, so it lives with the other rules.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<details>\n<summary><code>src/main.rs</code> · complete 422-line program</summary>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n        self.resolve_hit();\n    }\n\n    fn tick(&amp;mut self, seconds: u32) {\n        for _ in 0..seconds {\n            let damage = idle_damage_for_level(self.player.level);\n\n            self.enemy.health = health_after_hit(self.enemy.health, damage);\n            self.resolve_hit();\n        }\n    }\n\n    fn resolve_hit(&amp;mut self) {\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn idle_damage_for_level(level: u32) -&gt; u32 {\n    level\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n\n    let stage = game.stage;\n    let level = game.player.level;\n    let gold = game.player.gold;\n\n    game.tick(3600);\n\n    println!(\"One hour away.\");\n    println!(\n        \"Stage {} -&gt; {}  Level {} -&gt; {}  Gold {} -&gt; {}\",\n        stage, game.stage, level, game.player.level, gold, game.player.gold\n    );\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn idle_damage_grows_with_level() {\n        assert_eq!(idle_damage_for_level(1), 1);\n        assert_eq!(idle_damage_for_level(2), 2);\n    }\n\n    #[test]\n    fn enough_time_defeats_the_enemy() {\n        let mut game = Game::new();\n\n        game.tick(20);\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n\n    #[test]\n    fn one_second_is_one_idle_hit() {\n        let mut game = Game::new();\n\n        game.tick(1);\n\n        assert_eq!(game.enemy.health, 19);\n    }\n\n    #[test]\n    fn zero_seconds_changes_nothing() {\n        let mut game = Game::new();\n\n        game.tick(0);\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn one_long_tick_matches_many_short_ones() {\n        let mut long = Game::new();\n        let mut short = Game::new();\n\n        long.tick(45);\n\n        for _ in 0..45 {\n            short.tick(1);\n        }\n\n        assert_eq!(long.stage, short.stage);\n        assert_eq!(long.enemy.health, short.enemy.health);\n        assert_eq!(long.player.gold, short.player.gold);\n        assert_eq!(long.player.xp, short.player.xp);\n        assert_eq!(long.player.level, short.player.level);\n    }\n}\n</code></pre>\n</details>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear three stages, buy one upgrade, spend an hour away, and all twenty-one tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>time as a plain number, passed in by the caller</li>\n<li>the <code>tick</code> method</li>\n<li><code>for</code> loops and ranges like <code>0..seconds</code></li>\n<li><code>_</code> for a value we do not need</li>\n<li>extracting a shared method so two paths finish the same way</li>\n<li>tests that run an hour without waiting</li>\n</ul>\n<p>Two ideas are worth keeping.</p>\n<p>The first is how to choose between the two loops we now know.</p>\n<p>Ask whether you know the count.</p>\n<p>The level-up loop could not know how many levels one reward would cross, so it was a <code>while</code>.</p>\n<p><code>tick()</code> knows exactly how many seconds it was given, so it is a <code>for</code>.</p>\n<p>The second is <code>resolve_hit()</code>.</p>\n<p>The bug was not in the loop. The loop did what we told it.</p>\n<p>The bug was that two ways of hurting an enemy had to finish the same way, and only one of them did.</p>\n<p>Giving the finish a name fixed it, and now there is one place where \"the enemy went down\" turns into gold, XP, and a new slime.</p>\n<hr />\n<h2 id=\"you-accidentally-built-a-game-engine\">You accidentally built a game engine<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#you-accidentally-built-a-game-engine\" aria-label=\"Link to this section\">#</a></h2>\n<p>Open <code>src/main.rs</code> and scroll.</p>\n<p>Three structs. An enum. Three <code>impl</code> blocks. Twelve small rules. A click, a purchase, a clock that is not a clock, and twenty-one tests.</p>\n<p>All of it is in one file, and the file is getting long.</p>\n<p>Nobody planned that.</p>\n<p>Each article needed one more thing, and each thing went in the next free spot.</p>\n<p>But look at what is in there.</p>\n<p>Everything above <code>main()</code> knows nothing about a terminal, a screen, or a clock.</p>\n<p>Only <code>main()</code> does.</p>\n<p>Today they share a file. Next time, we put a wall between them, and find out that the part on the far side of the wall is already a small game engine.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/idle-is-not-a-timer/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Idle Is Not a Timer.\nCanonical: https://mnixblog.netlify.app/idle-is-not-a-timer/\nLet Your Rust Game Progress While You Are Away\nOur game has a progression loop now.\nKill a slime, get paid, level up, buy damage, kill a tougher slime.\nIt still stops the moment you stop clicking.\nSo today we are going to make time part of the game.\nNot a timer. Not a clock. Not sleep.\nJust a number.\nBecause the game will never look at a clock, we can test a full hour away in a fraction of a second.\nBy the end, an hour away from the game is one line:\ngame.tick(3600);\nAnd here is what the game looks like when you come back:\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nOur starting point\nIf you are only reading along, you can skip past this code to \"Time is just a number.\"\nIf you are building along, you need three structs, an enum, three impl blocks, eleven small functions, and sixteen tests:\n<details>\n<summary><code>src/main.rs</code> · 339-line starting point</summary>\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\n</details>\nRun it, and the player clears eight stages and buys three upgrades.\nThen the program is over, and so is all the progress, because nothing in it ever happens on its own.\nTime is just a number\nAn idle game has to keep going while nobody is looking.\nThe obvious way to build that is to give the game a clock.\nStart a timer. Every second, do some work. When the player leaves, keep the timer running.\nThat sounds simple, and it is a trap.\nA game that watches a clock is a game that has to be tested by waiting, unless you build a fake clock, and then you have rebuilt the idea we are about to use anyway.\nWant to know what happens after an hour away? Wait an hour.\nSo let's turn it around.\nThe game will not ask what time it is.\nWhoever is running the game will tell it how much time went by:\ngame.tick(10);\nTen seconds passed. What happened?\nGame code often uses the word tick for advancing simulation time, so we will borrow the name.\nThe contract is one sentence:\nTell me how many seconds passed. I will tell you what the game became.\ntick will be a method on Game, and the only thing it takes is that number.\nThe number is the only thing about time that Rust ever sees.\nLater in this series, a web page will measure real time and hand the game that number.\nFor now, our tests can hand it any number they like.\nQuestion: Why does tick(seconds) take a number instead of reading a clock?\nA test can hand it an hour of time and finish instantly\nRust has no way to read a clock at all\nA u32 automatically counts real seconds\nAnswer: 1. A test can hand it an hour of time and finish instantly\nExplanation: The game only sees a number, so whoever calls it decides how much time went by. A test can say 3600 without waiting.\nWhat does a player do while nobody is clicking?\nBefore the game can work out what happened, it needs to know what the player does with spare seconds.\nOur player is going to keep fighting.\nOne hit per second, and the harder the player has leveled up, the harder it hits.\nThat is a game rule, and we know what to do with game rules by now.\nGive it a small function, next to the others:\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nOne damage per second at level 1.\nTwo at level 2.\nIt is deliberately small. It is not balanced and it does not need to be yet.\nIt also ignores the click upgrades on purpose. Those are for clicking.\nWhat matters is that the rule has a name and a test.\nAdd it at the bottom of the tests module:\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\nNotice what the function does not take.\nIt does not take a Player. It does not take a Game.\nIt takes a level and returns a number. It is the same kind of function as base_damage_for_level(), and it asks the same question: what does this level of progress get you?\nIf you run cargo build now, Rust warns that idle_damage_for_level is never used.\nWe are about to use it.\nOne second at a time\nNow for tick().\nThe player hits once per second, so ten seconds is ten hits.\nWe know exactly how many times to repeat: once per second that passed.\nThat is not a job for while.\nA while loop keeps going until a condition stops being true, like our level-up loop, which cannot know how many levels one reward will cross.\nWhen you know the count in advance, Rust has a loop for that:\nfor _ in 0..seconds {\n// one second of work\n}\n0..seconds is a range.\nIt counts from 0 up to, but not including, seconds.\nSo 0..3 gives you 0, 1, and 2. That is three trips through the loop, which is exactly what we want for three seconds.\nTry it: run this, then change seconds to 0 and to 5, and count the trips.\nLive example · try changing the seconds away\nfn main() {\nlet seconds: u32 = 3;\nlet mut trips = 0;\nfor second in 0..seconds {\nprintln!(\"trip with range value {second}\");\ntrips += 1;\n}\nprintln!(\"{seconds} seconds away, {trips} trips through the loop\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/).\nWith 3 you should see range values 0, 1 and 2, then 3 seconds away, 3 trips through the loop. With 0 the loop never runs, and the last line says 0 trips.\nThe _ is a placeholder that means:\nI do not need this value.\nA for loop hands us the current number from the range on every trip.\nWe do not care which second we are on. We only care that one passed.\nIf we gave the number a name like second, Rust would warn that we never use it.\nThe _ says that is on purpose.\nQuestion: Why is tick() a for loop instead of a while loop?\nIt is given the number of seconds, so the count is known in advance\nA while loop can only run once\nA for loop skips the enemy health check\nAnswer: 1. It is given the number of seconds, so the count is known in advance\nExplanation: A while loop fits when you cannot know how many trips you need, like the level-up loop. Here the caller hands us the count.\nWrite the first version inside the impl Game block, right under click(). It is indented because it lives inside that block:\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\n}\n}\nEach trip through the loop works out the damage for the player's current level and hits the enemy with it.\nIt uses health_after_hit(), the same small rule that has been protecting enemy health since the first article.\nThat looks finished.\nIt is not, and we should have a test to say so.\nThe slime that died and kept being hit\nWe have a rule for how fast the player hits.\nWhat we need next is a test for the thing a player cares about: if I wait long enough, does the slime go down?\nAt level 1, our player hits for 1 per second, and the first slime has 20 health.\nTwenty seconds should do it:\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\nThose last numbers should look familiar.\nThey are the same ones the click version of this test checks: stage 2, a 30 health enemy, 5 gold, 6 XP.\nIt does not matter whether a slime dies from a click or from a wait.\nThe player should get paid the same either way.\nRun it:\ncargo test enough_time\nand it fails:\nassertion `left == right` failed\nleft: 1\nright: 2\n(Rust prints more than that, including a line number and a note about backtraces. This is the part that matters.)\nleft is what the game did. right is what the test expected.\nThe stage is still 1.\nTwenty seconds passed. The slime's health reached zero on the twentieth hit.\nAnd then nothing happened.\nOur loop never asks whether the enemy is defeated.\nThat check lives in click():\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\ntick() took the hit from click() but left the aftermath behind.\nQuestion: Why was the stage still 1 after the first version of tick(20)?\nThe slime's health reached zero, but tick() never checked is_defeated\n0..20 only runs 19 times, so the last hit never landed\nsaturating_sub wrapped around below zero\nAnswer: 1. The slime's health reached zero, but tick() never checked is_defeated\nExplanation: The loop landed all twenty hits. Only click() knew what to do afterward, so tick() needed the same check.\nTwo ways to land a hit\nThere are two ways to hurt an enemy now: a click, and a second of waiting.\nBoth of them need the same thing to happen afterward.\nIf we paste the if into tick(), we have the same three lines in two places.\nThen the next time defeating an enemy needs to change, we have to remember both.\nWe have seen this before.\nWhen the same enemy was being built in three different places, we gave the building a name, and Enemy::for_stage() became the one place that builds an enemy.\nSame fix here.\nAdd a method inside the same impl Game block, under tick():\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nRead it out loud:\nA hit just landed. If the enemy went down, handle the defeat.\nNow both ways of landing a hit can finish the same way.\nReplace the whole click() method with this one. The old if is gone, and a call to resolve_hit() takes its place:\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\nAnd replace tick() with this. Only the last line inside the loop is new:\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nRun the tests again:\ncargo test\nAll eighteen pass, including the clicking tests we did not touch.\nThat is what a good extraction looks like.\nclick() does the same thing it did before.\nIt just does it by calling a method that tick() can call too.\nNow follow the path of the twentieth second:\ntick()\n↓\nhealth_after_hit() → 0\n↓\nresolve_hit()\n↓\ndefeat_enemy()\n↓\ngold, XP, next stage, next enemy\nThat bottom half is the same code a click reaches.\nThe slime can die from a click or from a second of waiting.\nEither way, it dies the same way.\nTest time without waiting\nLook at what we just tested.\nTwenty seconds of game time ran, and the test finished instantly.\nNothing slept. Nothing waited.\nThe game does not know what a second is.\nIt was handed the number 20 and did twenty steps of work.\nThat is the payoff for the design.\nTime is an input, so a test can supply as much of it as it likes.\nThree more tests are worth adding. Put each one at the bottom of the tests module.\nOne second should be one hit:\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\nZero seconds should change nothing:\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\nWe did not write any code to handle zero.\nWe did not need to.\n0..0 is an empty range, so the loop runs zero times and the game stays exactly as it was.\nQuestion: Why does game.tick(0) leave the game unchanged without any special code for zero?\n0..0 is an empty range, so the loop body never runs\nhealth_after_hit refuses a damage of zero\nRust skips any method called with 0\nAnswer: 1. 0..0 is an empty range, so the loop body never runs\nExplanation: No trips through the loop means no damage, no defeat, and no payout.\nThe last test is the one that matters most.\nWaiting forty-five seconds in one go should give the same game as waiting one second forty-five times:\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\nThis one passes by construction today, because tick(45) literally is forty-five single steps.\nToday it almost looks redundant. Later, when we replace thousands of tiny steps with arithmetic, it becomes the test that proves the optimization did not change the game.\nIt is a promise to our future selves.\nLater, a browser will sometimes call tick(1) every second.\nOther times, it will come back after a long absence and call tick(7200) once.\nThose two have to be the same game.\nIf a change ever makes them different, this test will say so.\nTry it: here is a trimmed copy of our game with just tick(), resolve_hit() and the defeat rules. Change seconds and run it. Then try 3600.\nLive example · try changing the seconds away\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nstruct Enemy {\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Player {\nfn new() -> Player {\nPlayer { gold: 0, xp: 0, level: 1 }\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nhealth: stage.saturating_mul(10).saturating_add(10),\ngold_reward: stage.saturating_mul(2).saturating_add(3),\nxp_reward: stage.saturating_mul(2).saturating_add(4),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame { stage: 1, player: Player::new(), enemy: Enemy::for_stage(1) }\n}\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = self.enemy.health.saturating_sub(damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif self.enemy.health == 0 {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.gold = self.player.gold.saturating_add(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel.saturating_sub(1).saturating_mul(15).saturating_add(20)\n}\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nfn main() {\nlet seconds = 45;\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(seconds);\nfor _ in 0..seconds {\nshort.tick(1);\n}\nprintln!(\n\"One long tick:   stage {} health {} gold {} xp {} level {}\",\nlong.stage, long.enemy.health, long.player.gold, long.player.xp, long.player.level\n);\nprintln!(\n\"Many short ones: stage {} health {} gold {} xp {} level {}\",\nshort.stage, short.enemy.health, short.player.gold, short.player.xp, short.player.level\n);\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\nprintln!(\"Same game.\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/).\nWith 45 it prints the same line twice, stage 2 health 5 gold 5 xp 6 level 1, then Same game. The assert_eq! lines in main are our test; if the two games ever differ, the program stops with a panic instead.\nRun them all:\ncargo test\nTwenty-one tests should pass.\nLeave for an hour\nTime to try it for real.\nWe are going to leave the loop in main() exactly as it is, with one change: stop at stage 3, so the output is short.\nwhile game.stage <= 3 {\nEverything inside the loop stays the same.\nThen, after the loop, the player leaves.\nAdd this after the closing brace of the while loop, still inside main():\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\nThree thousand six hundred is the number of seconds in an hour.\nRun it:\ncargo run\nYou should see:\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nOne hour away.\nStage 4 -> 131  Level 2 -> 48  Gold 11 -> 17410\nThe first half is the player, clicking their way through three stages.\nThe last two lines are the hour.\nOne hour, with nobody at the keyboard, took the game from stage 4 to stage 131.\nThe player also came home with 17,410 gold and nothing spent. Noted.\nLook at the level.\nIt went from 2 to 48.\nThat was not a bug.\nEvery trip through the for loop asks for the damage at the player's current level.\nWhen a slime pays out enough XP, the player levels up in the middle of the hour, and every second after that hits a little harder.\nWaiting makes the player stronger, which makes waiting faster.\nThat is the shape of an idle game.\nIs it balanced? No.\nA stage 131 slime after an hour is a lot.\nWe will tune that later. We have a rule with a name and a test, so tuning it is changing one function.\nCoding challenge: Tune the idle hit\nChange idle_damage_for_level so that level 1 still hits for 1, but every level after that adds 2 more damage per second.\nKeep the function named idle_damage_for_level, taking a u32 level and returning a u32, because the checks call it. Check my code will tell you how you are doing.\nExpected result: the program prints Level 1: 1 damage per second, Level 2: 3 damage per second and Level 3: 5 damage per second.\nFinish the idle damage rule\nfn idle_damage_for_level(level: u32) -> u32 {\n// TODO: level 1 hits for 1, and every level after that adds 2 more\nlevel\n}\nfn main() {\nfor level in 1..=3 {\nprintln!(\"Level {level}: {} damage per second\", idle_damage_for_level(level));\n}\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// Level 1 is the base hit and gets nothing extra. Look at what your rule does with the first level.\n#[test]\nfn level_one_is_the_base_hit() {\nassert_eq!(idle_damage_for_level(1), 1);\n}\n/// Levels 2 and 3 should each step up from the level before. Check the size of the step.\n#[test]\nfn each_level_adds_two() {\nassert_eq!(idle_damage_for_level(2), 3);\nassert_eq!(idle_damage_for_level(3), 5);\n}\n/// The step should stay the same as levels climb. Try your rule on a level further out.\n#[test]\nfn later_levels_keep_the_step() {\nassert_eq!(idle_damage_for_level(10), 19);\nassert_eq!(idle_damage_for_level(100), 199);\n}\n/// Level 0 should not crash the game. Unsigned numbers do not like going below zero.\n#[test]\nfn level_zero_does_not_panic() {\nlet _ = idle_damage_for_level(0);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/).\nTip:\nStart from level.saturating_sub(1), like xp_to_next_level does, so level 1 gets no extra. Multiply that by 2 and add the 1 back.\nWhat matters today is that the engine can answer a question it could not answer before:\nWhat happened while I was away?\nWho owns the clock?\nThe game never asked what time it was.\nIt was handed 3600, and it did 3600 seconds of work.\nThat is going to become one of the most important lines in this series:\nRust owns the rules.\nWhatever runs the game owns time.\nToday, that caller is a test or main(). Later, it will be a web page.\nThe caller measures how much real time passed and hands that fact to the game.\nRust decides what that time means for gameplay.\nThat distinction matters. A browser might report that 28,800 seconds passed while the player was away. The game can still decide that only part of that time should count.\nIn other words:\ncaller measures elapsed time\n↓\nRust receives the number\n↓\nRust decides how much time counts\n↓\nRust applies the game rules\nThat keeps balance decisions inside the game instead of hiding them in whichever environment happens to run it.\nThere is one honest cost.\ntick() does one trip through the loop for every second.\nAn hour is 3,600 trips. A day is 86,400.\nThat is still manageable for this tiny program, but the cost grows directly with the number of seconds we simulate.\nA number like u32::MAX would be a very long walk.\nLater, we can cap credited away-time or replace repeated steps with arithmetic. The caller will still report elapsed time; Rust will still own the policy and the rules.\nQuestion: A browser reports 28,800 seconds away. Why should the game, not the caller, decide how many of them count?\nBalance rules belong in Rust, so every caller gets the same policy\nThe caller cannot measure elapsed time\ntick rejects any number above 3600\nAnswer: 1. Balance rules belong in Rust, so every caller gets the same policy\nExplanation: The caller measures; Rust decides what that time means. Capping away-time is a game rule, so it lives with the other rules.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\n<details>\n<summary><code>src/main.rs</code> · complete 422-line program</summary>\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nself.resolve_hit();\n}\nfn tick(&mut self, seconds: u32) {\nfor _ in 0..seconds {\nlet damage = idle_damage_for_level(self.player.level);\nself.enemy.health = health_after_hit(self.enemy.health, damage);\nself.resolve_hit();\n}\n}\nfn resolve_hit(&mut self) {\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn idle_damage_for_level(level: u32) -> u32 {\nlevel\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\nlet stage = game.stage;\nlet level = game.player.level;\nlet gold = game.player.gold;\ngame.tick(3600);\nprintln!(\"One hour away.\");\nprintln!(\n\"Stage {} -> {}  Level {} -> {}  Gold {} -> {}\",\nstage, game.stage, level, game.player.level, gold, game.player.gold\n);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn idle_damage_grows_with_level() {\nassert_eq!(idle_damage_for_level(1), 1);\nassert_eq!(idle_damage_for_level(2), 2);\n}\n#[test]\nfn enough_time_defeats_the_enemy() {\nlet mut game = Game::new();\ngame.tick(20);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n#[test]\nfn one_second_is_one_idle_hit() {\nlet mut game = Game::new();\ngame.tick(1);\nassert_eq!(game.enemy.health, 19);\n}\n#[test]\nfn zero_seconds_changes_nothing() {\nlet mut game = Game::new();\ngame.tick(0);\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn one_long_tick_matches_many_short_ones() {\nlet mut long = Game::new();\nlet mut short = Game::new();\nlong.tick(45);\nfor _ in 0..45 {\nshort.tick(1);\n}\nassert_eq!(long.stage, short.stage);\nassert_eq!(long.enemy.health, short.enemy.health);\nassert_eq!(long.player.gold, short.player.gold);\nassert_eq!(long.player.xp, short.player.xp);\nassert_eq!(long.player.level, short.player.level);\n}\n}\n</details>\nRun both:\ncargo run\ncargo test\nThe game should clear three stages, buy one upgrade, spend an hour away, and all twenty-one tests should pass.\nWhat we learned\nAlong the way, we encountered:\ntime as a plain number, passed in by the caller\nthe tick method\nfor loops and ranges like 0..seconds\n_ for a value we do not need\nextracting a shared method so two paths finish the same way\ntests that run an hour without waiting\nTwo ideas are worth keeping.\nThe first is how to choose between the two loops we now know.\nAsk whether you know the count.\nThe level-up loop could not know how many levels one reward would cross, so it was a while.\ntick() knows exactly how many seconds it was given, so it is a for.\nThe second is resolve_hit().\nThe bug was not in the loop. The loop did what we told it.\nThe bug was that two ways of hurting an enemy had to finish the same way, and only one of them did.\nGiving the finish a name fixed it, and now there is one place where \"the enemy went down\" turns into gold, XP, and a new slime.\nYou accidentally built a game engine\nOpen src/main.rs and scroll.\nThree structs. An enum. Three impl blocks. Twelve small rules. A click, a purchase, a clock that is not a clock, and twenty-one tests.\nAll of it is in one file, and the file is getting long.\nNobody planned that.\nEach article needed one more thing, and each thing went in the next free spot.\nBut look at what is in there.\nEverything above main() knows nothing about a terminal, a screen, or a clock.\nOnly main() does.\nToday they share a file. Next time, we put a wall between them, and find out that the part on the far side of the wall is already a small game engine.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/idle-is-not-a-timer/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-23T16:25:37Z",
      "id": "https://mnixblog.netlify.app/idle-is-not-a-timer/",
      "image": "https://mnixblog.netlify.app/images/idle-is-not-a-timer/cover.jpg",
      "summary": "Make your Rust idle game progress while you are away. Add tick(seconds), a for loop, and tests that run an hour of game time instantly.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Idle Is Not a Timer.",
      "url": "https://mnixblog.netlify.app/idle-is-not-a-timer/"
    },
    {
      "content_html": "<p>Our player earns gold now. After eight stages, they have ninety-six of it and nothing to spend it on.</p>\n<p>That is not a reward. That is a high score.</p>\n<p>So today we are going to sell them something: more damage per click, at a price that goes up every time they buy it.</p>\n<p>The shopping is the easy part.</p>\n<p>The interesting part is what happens when we try to tell the rest of the program whether the purchase worked.</p>\n<p>Because the obvious answer, <code>true</code> or <code>false</code>, turns out to be not quite an answer at all.</p>\n<pre><code class=\"language-text\">Bought click upgrade 1! Click damage: 7  Gold left: 2\n</code></pre>\n<p>Along the way: four quick knowledge checks, two live examples you can edit and run in the page, and a coding challenge at the end.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-free-upgrade-nobody-earned\">skip past this code to \"The free upgrade nobody earned.\"</a></p>\n<p>If you are building along, you need three structs, three <code>impl</code> blocks, nine small functions, and thirteen tests:</p>\n<details>\n<summary><code>src/main.rs</code> · 268-line starting point</summary>\n<pre><code class=\"language-rust\">struct Player {\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            bonus_damage: 2,\n            gold: 0,\n            xp: 0,\n            level: 1,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage)\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n</details>\n<p><code>Player</code> stores what the player has earned: gold, XP, and a level. Defeating an enemy pays gold and XP, and enough XP levels the player up.</p>\n<p>Click damage comes from two places. <code>base_damage()</code> works it out from the player's level. <code>bonus_damage</code> is just a number stored in the struct.</p>\n<p>Run <code>cargo test</code> and all thirteen should pass.</p>\n<hr />\n<h2 id=\"the-free-upgrade-nobody-earned\">The free upgrade nobody earned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-free-upgrade-nobody-earned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at that <code>Player</code> again.</p>\n<pre><code class=\"language-rust\">    bonus_damage: u32,\n</code></pre>\n<p>Every new player starts with 2 of it. For free. Since the first time we gave the player a second damage number.</p>\n<p><code>base_damage</code> is already a method, not a field. It is worked out from the player's level, because it is a consequence of that level, not a fact to store.</p>\n<p><code>bonus_damage</code> has the same problem, with a different cause.</p>\n<p>Bonus damage should be a consequence of how many upgrades the player has bought.</p>\n<p>So the player does not need to store the bonus. The player needs to store the shopping.</p>\n<p>Two rules, under <code>base_damage_for_level()</code>:</p>\n<pre><code class=\"language-rust\">fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n</code></pre>\n<p>Each upgrade adds 2 damage:</p>\n<pre><code class=\"language-text\">0 upgrades → 0 bonus\n1 upgrade  → 2 bonus\n2 upgrades → 4 bonus\n</code></pre>\n<p>And each one costs more than the last:</p>\n<pre><code class=\"language-text\">first upgrade  → 10 gold\nsecond upgrade → 20 gold\nthird upgrade  → 30 gold\n</code></pre>\n<p>Both rules take the number of upgrades already bought. Neither one knows a <code>Player</code> exists.</p>\n<p>If you build now, Rust warns that both are never used. The next few steps use them.</p>\n<p>Want to feel the price curve before you commit to it? Edit the numbers and press Run.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · tune the shop</figcaption><pre><code class=\"language-rust\">fn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn main() {\n    for upgrades in 0..5 {\n        println!(\n            &quot;{upgrades} bought: +{} damage, next one costs {} gold&quot;,\n            click_upgrade_bonus(upgrades),\n            click_upgrade_cost(upgrades)\n        );\n    }\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\">the original article</a>.</p></figure>\n<p>Try <code>saturating_mul(3)</code> for a stronger upgrade, or <code>saturating_mul(25)</code> for a pricier one. Keep the original numbers in your project, though. The run output later in this article depends on them.</p>\n<p>Now replace the field with a count of purchases:</p>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n</code></pre>\n<p>Rust immediately complains about every place that still mentions <code>bonus_damage</code>. The next few steps clear those errors one at a time.</p>\n<p>A new player has bought nothing:</p>\n<pre><code class=\"language-rust\">            click_upgrade_level: 0,\n</code></pre>\n<p>And just like base damage, bonus damage becomes a method that asks the rule. Put it under <code>base_damage()</code>:</p>\n<pre><code class=\"language-rust\">    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n</code></pre>\n<p><code>attack()</code> gets its second pair of parentheses:</p>\n<pre><code class=\"language-rust\">    let damage = click_damage(player.base_damage(), player.bonus_damage());\n</code></pre>\n<p>One more place still reads the old field: the \"Level up!\" line in <code>main()</code>. Rust points straight at it (the line is wrapped here to fit):</p>\n<pre><code class=\"language-text\">error[E0615]: attempted to take value of method\n`bonus_damage` on type `Player`\n</code></pre>\n<p>It even suggests the fix: use parentheses to call the method.</p>\n<pre><code class=\"language-rust\">                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n</code></pre>\n<p>Now it builds again.</p>\n<p>Notice what <code>Player</code> is made of now.</p>\n<pre><code class=\"language-text\">Player\n├── gold\n├── xp\n├── level\n└── click_upgrade_level\n</code></pre>\n<p>Not a single damage number.</p>\n<p>Every field is a record of <strong>progress</strong>: what the player has earned, and what they have done with it.</p>\n<p>The damage numbers are worked out from that progress, by rules, on demand.</p>\n<p>That is going to matter a great deal in a few articles, when we save the game.</p>\n<p>We will only have to save what the player earned. Everything else can be recalculated.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>Player</code> now store <code>click_upgrade_level</code> instead of <code>bonus_damage</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust does not allow a struct to store a damage number</li>\n<li>The bonus is a consequence of what the player bought, so a rule can work it out</li>\n<li>Calling a method is faster than reading a field</li>\n<li>So the player can never buy more than one upgrade</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The bonus is a consequence of what the player bought, so a rule can work it out</p>\n<p>Store the progress, compute the rest. The number of purchases is the fact, and <code>click_upgrade_bonus()</code> turns it into damage whenever we ask.</p>\n</div>\n</div>\n</section>\n<h3 id=\"the-first-fight-changes\">The first fight changes<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-first-fight-changes\" aria-label=\"Link to this section\">#</a></h3>\n<p>One honest side effect.</p>\n<p>A new player used to hit for 7: 5 base plus 2 free bonus.</p>\n<p>Now they hit for 5, because the bonus has to be bought.</p>\n<p>The stage 1 slime still has 20 health, so it takes four clicks instead of three.</p>\n<p>Two of our tests notice.</p>\n<p><code>clicking_damages_the_enemy</code> expected 13 health after one click. It is 15 now.</p>\n<p><code>attack_reduces_enemy_health</code> expected 3 health left from 10. It is 5.</p>\n<p>Update both numbers and run <code>cargo test</code>.</p>\n<p>Those tests were not broken. The game changed, and the tests were telling the truth about it.</p>\n<hr />\n<h2 id=\"buying-the-upgrade\">Buying the upgrade<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#buying-the-upgrade\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the first version of the purchase. Add it to <code>impl Player</code>, under <code>bonus_damage()</code>:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; bool {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return false;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        true\n    }\n</code></pre>\n<p>Work out the price.</p>\n<p>If the player cannot afford it, stop, and say so.</p>\n<p>Otherwise, take the money, add the upgrade, and say so.</p>\n<p>Two things to notice.</p>\n<p><code>return false;</code> leaves the method early.</p>\n<p>Everything after it only runs if the player could afford the upgrade.</p>\n<p>That lets the rest of the method read without an <code>else</code> wrapped around it.</p>\n<p>The <code>true</code> on the last line needs no <code>return</code>. It is the same rule every function so far has used: the last expression, with no semicolon, is the value the function gives back. <code>return</code> is only for leaving from somewhere in the middle.</p>\n<p>And <code>self.gold -= cost;</code> is a plain minus sign, like the one in <code>add_xp()</code>.</p>\n<p>The check right above it guarantees <code>self.gold</code> is at least <code>cost</code>, so the subtraction cannot go below zero.</p>\n<p>Then <code>Game</code> passes the request along, inside <code>impl Game</code>:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; bool {\n        self.player.buy_click_upgrade()\n    }\n</code></pre>\n<p>The game does not know the price of an upgrade.</p>\n<p>It does not know what happens when you buy one.</p>\n<p>It knows the player handles that.</p>\n<p>Nothing calls either method yet, so Rust warns that they are never used, and <code>click_upgrade_cost()</code> along with them. <code>main()</code> will fix that shortly.</p>\n<hr />\n<h2 id=\"false-is-not-an-answer\"><code>false</code> is not an answer<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#false-is-not-an-answer\" aria-label=\"Link to this section\">#</a></h2>\n<p>This works.</p>\n<p>Now imagine using it:</p>\n<pre><code class=\"language-rust\">if game.buy_click_upgrade() {\n    println!(\"Bought it!\");\n} else {\n    println!(\"???\");\n}\n</code></pre>\n<p>What goes in the <code>else</code>?</p>\n<p><code>false</code> means the purchase did not happen.</p>\n<p>It does not say <em>why</em>.</p>\n<p>Today there is only one reason: not enough gold.</p>\n<p>But look at this from the other side.</p>\n<p>The code calling <code>buy_click_upgrade()</code> has to <em>remember</em> that <code>false</code> means \"not enough gold.\"</p>\n<p>Nothing in the code says so.</p>\n<p>Add a second reason to fail someday, say a maximum upgrade level, and <code>false</code> starts meaning two different things.</p>\n<p>Every place that checked it is now quietly wrong, and Rust will not say a word.</p>\n<p>We want an answer that says what happened in plain words, and that Rust can check for us.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>buy_click_upgrade()</code> returns <code>false</code>. What does the calling code actually know?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>That the player did not have enough gold</li>\n<li>Only that the purchase did not happen</li>\n<li>That the method crashed</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Only that the purchase did not happen</p>\n<p><code>false</code> says no, and nothing else. The reason lives in the programmer's memory, not in the code, and a second way to fail would hide behind the same <code>false</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"give-the-outcome-a-name\">Give the outcome a name<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#give-the-outcome-a-name\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust has a type for \"one of these specific things\":</p>\n<pre><code class=\"language-rust\">#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n</code></pre>\n<p>Put it with the structs, after <code>Game</code>. It is a type, like they are. Until something uses it, Rust adds one more never-used warning to the pile.</p>\n<p><code>enum</code> is short for <em>enumeration</em>: a type whose values are listed out, one by one.</p>\n<p>A <code>PurchaseOutcome</code> is either <code>Purchased</code> or <code>NotEnoughGold</code>.</p>\n<p>It cannot be anything else.</p>\n<p>It cannot be both.</p>\n<p>The two options are called <strong>variants</strong>, and you name one the same way you reach into a type for a function:</p>\n<pre><code class=\"language-rust\">PurchaseOutcome::NotEnoughGold\n</code></pre>\n<p>You can think about <code>bool</code> the same way for this purpose: it also stands for one of exactly two possibilities, <code>true</code> or <code>false</code>.</p>\n<p>The difference is that our enum lets us give those possibilities names that belong to our game.</p>\n<h3 id=\"the-line-above-the-enum\">The line above the enum<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-line-above-the-enum\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-rust\">#[derive(Debug, PartialEq)]\n</code></pre>\n<p><code>PartialEq</code> is what lets <code>==</code> compare two outcomes. Without it, Rust has no idea whether <code>Purchased == Purchased</code>.</p>\n<p><code>Debug</code> is what lets Rust print an outcome for a programmer to read, so a failing test can show <code>NotEnoughGold</code> in its message.</p>\n<p>Numbers like <code>u32</code> come with both abilities built in. Our brand-new enum starts with neither.</p>\n<p>Rust calls abilities like these <strong>traits</strong>. That is all you need about traits today: a trait is a named ability a type can have, like \"can be compared with <code>==</code>.\"</p>\n<p><code>#[derive(...)]</code> is the one-line way to ask Rust for the standard version of a trait, instead of writing it by hand.</p>\n<p>Our tests will need both.</p>\n<h3 id=\"use-it\">Use it<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#use-it\" aria-label=\"Link to this section\">#</a></h3>\n<p>Now the purchase can say what happened:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n</code></pre>\n<p>Same shape. Same early return.</p>\n<p>Only the answers changed, and now they read like the game talking.</p>\n<p><code>Game::buy_click_upgrade()</code> changes its return type to match:</p>\n<pre><code class=\"language-rust\">    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n</code></pre>\n<hr />\n<h2 id=\"match-one-branch-per-answer\"><code>match</code>: one branch per answer<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#match-one-branch-per-answer\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust's <code>match</code> expression gives us a way to handle each possible variant explicitly.</p>\n<p>Here is how <code>main()</code> will react to a purchase:</p>\n<pre><code class=\"language-rust\">match game.buy_click_upgrade() {\n    PurchaseOutcome::Purchased =&gt; println!(\"Bought it!\"),\n    PurchaseOutcome::NotEnoughGold =&gt; {}\n}\n</code></pre>\n<p><code>match</code> looks at the value and runs the branch that fits.</p>\n<p>Each line is called an <strong>arm</strong>: a pattern, an arrow, and what to do.</p>\n<p>That arrow is <code>=&gt;</code>, a fat arrow. It is not the <code>-&gt;</code> from function signatures, and your fingers will try to type that one at least once.</p>\n<p>Arms end with a comma, like items in a list. An arm whose body is a block in braces can leave the comma off.</p>\n<p>The <code>{}</code> on the second branch means: do nothing.</p>\n<p>That is allowed. What is not allowed is leaving a variant out.</p>\n<p>Delete the <code>NotEnoughGold</code> line and Rust refuses to compile:</p>\n<pre><code class=\"language-text\">error[E0004]: non-exhaustive patterns:\n`PurchaseOutcome::NotEnoughGold` not covered\n</code></pre>\n<p>This is the payoff.</p>\n<p>With our <code>bool</code> version, the caller had to remember what <code>false</code> meant.</p>\n<p>With an enum and <code>match</code>, the possible outcomes are named, and Rust can make us handle every one.</p>\n<p>If we add a third variant next month, every <code>match</code> that names the variants will stop compiling until it says what to do about it.</p>\n<p>Rust is keeping a list for us.</p>\n<p>The full error message also offers to fill the gap for you, with a <em>wildcard pattern</em> or a <code>todo!()</code> placeholder that crashes if it ever runs. A wildcard (<code>_ =&gt;</code>, meaning \"anything else\") would compile, but it switches this check off for every variant you add later. Naming each variant is what keeps the list.</p>\n<p>Try it. Run this, then delete one arm and run it again. Then put the arm back, add a third variant to the enum, and run it without touching the <code>match</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · break the match on purpose</figcaption><pre><code class=\"language-rust\">enum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nfn try_to_buy(gold: u32, cost: u32) -&gt; PurchaseOutcome {\n    if gold &lt; cost {\n        return PurchaseOutcome::NotEnoughGold;\n    }\n\n    PurchaseOutcome::Purchased\n}\n\nfn main() {\n    for gold in [5, 10, 15] {\n        match try_to_buy(gold, 10) {\n            PurchaseOutcome::Purchased =&gt; println!(&quot;{gold} gold: bought it!&quot;),\n            PurchaseOutcome::NotEnoughGold =&gt; println!(&quot;{gold} gold: not enough.&quot;),\n        }\n    }\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You add a <code>MaxLevelReached</code> variant to <code>PurchaseOutcome</code>. What does Rust do the next time you build?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Nothing, until a purchase actually returns <code>MaxLevelReached</code></li>\n<li>It refuses to compile the enum itself</li>\n<li>Every <code>match</code> that names the variants, but not the new one, stops compiling</li>\n<li>Every <code>match</code> quietly skips the new variant</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. Every <code>match</code> that names the variants, but not the new one, stops compiling</p>\n<p>The check happens when the program is built, not when it runs. The <code>match</code> in <code>main()</code> gets an <code>E0004</code> error until it handles the new variant.</p>\n</div>\n</div>\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Rust reports <code>E0004</code> for a missing arm, and you silence it by adding <code>_ =&gt; {}</code>. What have you given up?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The existing arms stop running</li>\n<li>Rust will no longer tell you when a variant added later needs handling</li>\n<li>The <code>match</code> no longer compiles</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Rust will no longer tell you when a variant added later needs handling</p>\n<p><code>_</code> means \"anything else\", so a future variant lands in it silently. Naming every variant is what keeps Rust's list.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"main-goes-shopping\"><code>main()</code> goes shopping<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#main-goes-shopping\" aria-label=\"Link to this section\">#</a></h2>\n<p>Add the purchase to the loop, after the stage-cleared block:</p>\n<pre><code class=\"language-rust\">        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n</code></pre>\n<p>Every turn, <code>main()</code> tries to buy an upgrade, and only says something when it works.</p>\n<p>Worth being clear about what that means: in this terminal version, the game buys an upgrade the moment the player can afford one. Nobody is deciding anything.</p>\n<p>Later, when this game has a button, the player will choose when to spend. The purchasing rule does not change when that happens. Only who calls it.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 22  XP: 16/35\n  Next up: Training Slime with 60 health.\nBought click upgrade 2! Click damage: 10  Gold left: 2\nStage 5 cleared!  Gold: 15  XP: 30/35\n  Next up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 30  XP: 11/50\nLevel up! Now level 3. Click damage: 11\n  Next up: Training Slime with 80 health.\nBought click upgrade 3! Click damage: 13  Gold left: 0\nStage 7 cleared!  Gold: 17  XP: 29/50\n  Next up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 36  XP: 49/50\n  Next up: Training Slime with 100 health.\n</code></pre>\n<p>Now gold goes somewhere.</p>\n<p>It climbs, and then it drops to almost nothing, because something got bought.</p>\n<p>Click damage goes 5 → 7 → 8 → 10 → 11 → 13, pushed up from two directions at once: levels the player earns, and upgrades the player buys.</p>\n<p>Compare the last line to where we started this article. Ninety-six gold and nothing to do with it, versus thirty-six gold and three upgrades already paid for.</p>\n<hr />\n<h2 id=\"testing-the-purchase\">Testing the purchase<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#testing-the-purchase\" aria-label=\"Link to this section\">#</a></h2>\n<p>The pricing rule gets a test for its numbers. Put the new tests in <code>mod tests</code>, after <code>levels_make_the_player_hit_harder</code>:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n</code></pre>\n<p>Then the two things that can happen.</p>\n<p>A broke player cannot buy anything, and a failed purchase changes nothing:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n</code></pre>\n<p>Those <code>assert_eq!</code> lines comparing outcomes are exactly why we asked for <code>PartialEq</code> and <code>Debug</code>.</p>\n<p><code>assert_eq!</code> needs both. <code>PartialEq</code> does the comparison. <code>Debug</code> prints the two sides if they do not match. Leave either one out of the <code>derive</code> line and these tests will not even compile.</p>\n<p><code>cargo run</code> would still build without them, because <code>match</code> needs neither. It is the tests that ask.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You delete <code>Debug</code> from <code>#[derive(Debug, PartialEq)]</code>. What happens to the test that runs <code>assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased)</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It compiles, but a failing test prints an empty message</li>\n<li>The test code no longer compiles</li>\n<li>Nothing changes</li>\n<li>It panics when the test runs</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The test code no longer compiles</p>\n<p><code>assert_eq!</code> has to be able to print both sides in case they differ, so it needs <code>Debug</code> before the test can even be built.</p>\n</div>\n</div>\n</section>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All sixteen should pass.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#your-turn-cap-the-upgrades\">skip ahead to the coding challenge</a>.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<details>\n<summary><code>src/main.rs</code> · complete 339-line program</summary>\n<pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    xp: u32,\n    level: u32,\n    click_upgrade_level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            gold: 0,\n            xp: 0,\n            level: 1,\n            click_upgrade_level: 0,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n\n    fn bonus_damage(&amp;self) -&gt; u32 {\n        click_upgrade_bonus(self.click_upgrade_level)\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        self.player.buy_click_upgrade()\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn click_upgrade_bonus(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_mul(2)\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage());\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage())\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n\n        match game.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                \"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\n                game.player.click_upgrade_level,\n                click_damage(game.player.base_damage(), game.player.bonus_damage()),\n                game.player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; {}\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 5);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn upgrades_cost_more_each_time() {\n        assert_eq!(click_upgrade_cost(0), 10);\n        assert_eq!(click_upgrade_cost(1), 20);\n    }\n\n    #[test]\n    fn buying_an_upgrade_needs_gold() {\n        let mut player = Player::new();\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\n        assert_eq!(player.click_upgrade_level, 0);\n        assert_eq!(player.bonus_damage(), 0);\n    }\n\n    #[test]\n    fn buying_an_upgrade_spends_gold() {\n        let mut player = Player::new();\n        player.gold = 12;\n\n        assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\n        assert_eq!(player.gold, 2);\n        assert_eq!(player.click_upgrade_level, 1);\n        assert_eq!(player.bonus_damage(), 2);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 15);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n</details>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear eight stages, buying three upgrades along the way, and all sixteen tests should pass.</p>\n<hr />\n<h2 id=\"your-turn-cap-the-upgrades\">Your turn: cap the upgrades<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#your-turn-cap-the-upgrades\" aria-label=\"Link to this section\">#</a></h2>\n<p>A third way for a purchase to fail is exactly what the enum was built for. This is your chance to watch Rust walk you to every place that needs to know.</p>\n<p><strong>Coding challenge: a maxed-out upgrade</strong></p>\n<p>Stop selling click upgrades after the fifth one.</p>\n<ol>\n<li>Add a third variant to <code>PurchaseOutcome</code>, for an upgrade that is maxed out.</li>\n<li>In <code>buy_click_upgrade()</code>, return that variant when the player already has 5 upgrades. Check this before the price.</li>\n<li>Press Run. Rust will refuse to compile until the <code>match</code> in <code>main()</code> handles your new variant. Add that arm.</li>\n</ol>\n<p>Keep <code>Player</code>, its two fields and <code>buy_click_upgrade()</code> as they are, because the checks call them. <strong>Check my code</strong> will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> five \"Bought upgrade\" lines, then two lines saying the upgrades are maxed out. Gold left stops at 850.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Coding challenge · cap the upgrades</figcaption><pre><code class=\"language-rust\">struct Player {\n    gold: u32,\n    click_upgrade_level: u32,\n}\n\nenum PurchaseOutcome {\n    Purchased,\n    NotEnoughGold,\n    // TODO 1: add a third variant for &quot;this upgrade is maxed out&quot;\n}\n\nimpl Player {\n    fn buy_click_upgrade(&amp;mut self) -&gt; PurchaseOutcome {\n        // TODO 2: stop at 5 upgrades, before checking the price\n\n        let cost = click_upgrade_cost(self.click_upgrade_level);\n\n        if self.gold &lt; cost {\n            return PurchaseOutcome::NotEnoughGold;\n        }\n\n        self.gold -= cost;\n        self.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\n\n        PurchaseOutcome::Purchased\n    }\n}\n\nfn click_upgrade_cost(upgrades: u32) -&gt; u32 {\n    upgrades.saturating_add(1).saturating_mul(10)\n}\n\nfn main() {\n    let mut player = Player {\n        gold: 1000,\n        click_upgrade_level: 0,\n    };\n\n    for _ in 0..7 {\n        match player.buy_click_upgrade() {\n            PurchaseOutcome::Purchased =&gt; println!(\n                &quot;Bought upgrade {}. Gold left: {}&quot;,\n                player.click_upgrade_level, player.gold\n            ),\n            PurchaseOutcome::NotEnoughGold =&gt; println!(&quot;Not enough gold.&quot;),\n            // TODO 3: tell the player the upgrade is maxed out\n        }\n    }\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// Once a player holds 5 upgrades, buying again should leave their gold and level alone. Look at what your function does at the cap.\n#[test]\nfn sixth_upgrade_is_refused() {\n    let mut player = Player {\n        gold: 1000,\n        click_upgrade_level: 5,\n    };\n    player.buy_click_upgrade();\n    assert_eq!(player.gold, 1000);\n    assert_eq!(player.click_upgrade_level, 5);\n}\n\n/// A player at the cap with an empty purse is maxed out, not broke. Which check comes first?\n#[test]\nfn cap_is_checked_before_price() {\n    let mut player = Player {\n        gold: 0,\n        click_upgrade_level: 5,\n    };\n    let outcome = player.buy_click_upgrade();\n    assert!(!matches!(outcome, PurchaseOutcome::NotEnoughGold));\n    assert!(!matches!(outcome, PurchaseOutcome::Purchased));\n}\n\n/// The fifth upgrade still has to sell. Look at where your cap starts.\n#[test]\nfn fifth_upgrade_still_sells() {\n    let mut player = Player {\n        gold: 1000,\n        click_upgrade_level: 4,\n    };\n    let outcome = player.buy_click_upgrade();\n    assert!(matches!(outcome, PurchaseOutcome::Purchased));\n    assert_eq!(player.click_upgrade_level, 5);\n    assert_eq!(player.gold, 950);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Step 2 is an early <code>return</code>, just like the <code>NotEnoughGold</code> check below it. Compare <code>self.click_upgrade_level</code> with 5 using <code>&gt;=</code>.</p>\n</aside>\n<details>\n<summary>Show one solution</summary>\n<p>The new variant:</p>\n<pre><code class=\"language-rust\">    MaxedOut,\n</code></pre>\n<p>The early return, at the top of <code>buy_click_upgrade()</code>:</p>\n<pre><code class=\"language-rust\">        if self.click_upgrade_level &gt;= 5 {\n            return PurchaseOutcome::MaxedOut;\n        }\n</code></pre>\n<p>The new arm in <code>main()</code>:</p>\n<pre><code class=\"language-rust\">            PurchaseOutcome::MaxedOut =&gt; println!(\"Click upgrades are maxed out.\"),\n</code></pre>\n</details>\n<p>The challenge is its own small program, so your <code>src/main.rs</code> stays exactly as it is above.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>a second rule-derived value, replacing a stored one</li>\n<li>early <code>return</code></li>\n<li><code>enum</code> and variants</li>\n<li>traits, in one sentence, and <code>#[derive(Debug, PartialEq)]</code></li>\n<li><code>match</code>, and the compiler refusing to let us forget a case</li>\n</ul>\n<p>Two ideas are worth keeping.</p>\n<p>The first finishes the job <code>base_damage()</code> started. <code>Player</code> has no damage numbers in it at all now. It stores progress, and damage is a rule applied to that progress.</p>\n<p>The second is <code>PurchaseOutcome</code>.</p>\n<p>When a <code>bool</code> would have made the caller remember what <code>false</code> meant, we gave the answers names instead, and Rust started checking that every answer gets handled.</p>\n<p>That pattern shows up everywhere in Rust. Two common examples you will eventually meet are <code>Option</code>, for a value that might not be there, and <code>Result</code>, for an operation that might have failed.</p>\n<p>This is the first time we needed our own. It will not be the last.</p>\n<hr />\n<h2 id=\"nothing-happens-unless-you-click\">Nothing happens unless you click<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#nothing-happens-unless-you-click\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game finally has a progression loop.</p>\n<p>Kill a slime, get paid, level up, buy damage, kill a tougher slime.</p>\n<p>There is just one problem with calling it an idle game.</p>\n<pre><code class=\"language-text\">clicks per second while you are playing: a lot\nclicks per second while you are not:    zero\n</code></pre>\n<p>Nothing happens unless you click.</p>\n<p>Walk away, come back an hour later, and the slime is exactly where you left it.</p>\n<p>An idle game is supposed to keep working while you do not.</p>\n<p>Next time, we are going to give the game time.</p>\n<p>Not a timer. Not a clock. Just a number: how many seconds went by.</p>\n<p>The game will take that number and work out everything that should have happened.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "False Is Not an Answer.\nCanonical: https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/\nBuild Click Upgrades With Rust Enums and Match\nOur player earns gold now. After eight stages, they have ninety-six of it and nothing to spend it on.\nThat is not a reward. That is a high score.\nSo today we are going to sell them something: more damage per click, at a price that goes up every time they buy it.\nThe shopping is the easy part.\nThe interesting part is what happens when we try to tell the rest of the program whether the purchase worked.\nBecause the obvious answer, true or false, turns out to be not quite an answer at all.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nAlong the way: four quick knowledge checks, two live examples you can edit and run in the page, and a coding challenge at the end.\nOur starting point\nIf you are only reading along, skip past this code to \"The free upgrade nobody earned.\" (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#the-free-upgrade-nobody-earned)\nIf you are building along, you need three structs, three impl blocks, nine small functions, and thirteen tests:\n<details>\n<summary><code>src/main.rs</code> · 268-line starting point</summary>\nstruct Player {\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\nbonus_damage: 2,\ngold: 0,\nxp: 0,\nlevel: 1,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage)\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\n</details>\nPlayer stores what the player has earned: gold, XP, and a level. Defeating an enemy pays gold and XP, and enough XP levels the player up.\nClick damage comes from two places. base_damage() works it out from the player's level. bonus_damage is just a number stored in the struct.\nRun cargo test and all thirteen should pass.\nThe free upgrade nobody earned\nLook at that Player again.\nbonus_damage: u32,\nEvery new player starts with 2 of it. For free. Since the first time we gave the player a second damage number.\nbase_damage is already a method, not a field. It is worked out from the player's level, because it is a consequence of that level, not a fact to store.\nbonus_damage has the same problem, with a different cause.\nBonus damage should be a consequence of how many upgrades the player has bought.\nSo the player does not need to store the bonus. The player needs to store the shopping.\nTwo rules, under base_damage_for_level():\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nEach upgrade adds 2 damage:\n0 upgrades → 0 bonus\n1 upgrade  → 2 bonus\n2 upgrades → 4 bonus\nAnd each one costs more than the last:\nfirst upgrade  → 10 gold\nsecond upgrade → 20 gold\nthird upgrade  → 30 gold\nBoth rules take the number of upgrades already bought. Neither one knows a Player exists.\nIf you build now, Rust warns that both are never used. The next few steps use them.\nWant to feel the price curve before you commit to it? Edit the numbers and press Run.\nLive example · tune the shop\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn main() {\nfor upgrades in 0..5 {\nprintln!(\n\"{upgrades} bought: +{} damage, next one costs {} gold\",\nclick_upgrade_bonus(upgrades),\nclick_upgrade_cost(upgrades)\n);\n}\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nTry saturating_mul(3) for a stronger upgrade, or saturating_mul(25) for a pricier one. Keep the original numbers in your project, though. The run output later in this article depends on them.\nNow replace the field with a count of purchases:\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nRust immediately complains about every place that still mentions bonus_damage. The next few steps clear those errors one at a time.\nA new player has bought nothing:\nclick_upgrade_level: 0,\nAnd just like base damage, bonus damage becomes a method that asks the rule. Put it under base_damage():\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nattack() gets its second pair of parentheses:\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nOne more place still reads the old field: the \"Level up!\" line in main(). Rust points straight at it (the line is wrapped here to fit):\nerror[E0615]: attempted to take value of method\n`bonus_damage` on type `Player`\nIt even suggests the fix: use parentheses to call the method.\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\nNow it builds again.\nNotice what Player is made of now.\nPlayer\n├── gold\n├── xp\n├── level\n└── click_upgrade_level\nNot a single damage number.\nEvery field is a record of progress: what the player has earned, and what they have done with it.\nThe damage numbers are worked out from that progress, by rules, on demand.\nThat is going to matter a great deal in a few articles, when we save the game.\nWe will only have to save what the player earned. Everything else can be recalculated.\nQuestion: Why does Player now store click_upgrade_level instead of bonus_damage?\nRust does not allow a struct to store a damage number\nThe bonus is a consequence of what the player bought, so a rule can work it out\nCalling a method is faster than reading a field\nSo the player can never buy more than one upgrade\nAnswer: 2. The bonus is a consequence of what the player bought, so a rule can work it out\nExplanation: Store the progress, compute the rest. The number of purchases is the fact, and click_upgrade_bonus() turns it into damage whenever we ask.\nThe first fight changes\nOne honest side effect.\nA new player used to hit for 7: 5 base plus 2 free bonus.\nNow they hit for 5, because the bonus has to be bought.\nThe stage 1 slime still has 20 health, so it takes four clicks instead of three.\nTwo of our tests notice.\nclicking_damages_the_enemy expected 13 health after one click. It is 15 now.\nattack_reduces_enemy_health expected 3 health left from 10. It is 5.\nUpdate both numbers and run cargo test.\nThose tests were not broken. The game changed, and the tests were telling the truth about it.\nBuying the upgrade\nHere is the first version of the purchase. Add it to impl Player, under bonus_damage():\nfn buy_click_upgrade(&mut self) -> bool {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn false;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\ntrue\n}\nWork out the price.\nIf the player cannot afford it, stop, and say so.\nOtherwise, take the money, add the upgrade, and say so.\nTwo things to notice.\nreturn false; leaves the method early.\nEverything after it only runs if the player could afford the upgrade.\nThat lets the rest of the method read without an else wrapped around it.\nThe true on the last line needs no return. It is the same rule every function so far has used: the last expression, with no semicolon, is the value the function gives back. return is only for leaving from somewhere in the middle.\nAnd self.gold -= cost; is a plain minus sign, like the one in add_xp().\nThe check right above it guarantees self.gold is at least cost, so the subtraction cannot go below zero.\nThen Game passes the request along, inside impl Game:\nfn buy_click_upgrade(&mut self) -> bool {\nself.player.buy_click_upgrade()\n}\nThe game does not know the price of an upgrade.\nIt does not know what happens when you buy one.\nIt knows the player handles that.\nNothing calls either method yet, so Rust warns that they are never used, and click_upgrade_cost() along with them. main() will fix that shortly.\nfalse is not an answer\nThis works.\nNow imagine using it:\nif game.buy_click_upgrade() {\nprintln!(\"Bought it!\");\n} else {\nprintln!(\"???\");\n}\nWhat goes in the else?\nfalse means the purchase did not happen.\nIt does not say why.\nToday there is only one reason: not enough gold.\nBut look at this from the other side.\nThe code calling buy_click_upgrade() has to remember that false means \"not enough gold.\"\nNothing in the code says so.\nAdd a second reason to fail someday, say a maximum upgrade level, and false starts meaning two different things.\nEvery place that checked it is now quietly wrong, and Rust will not say a word.\nWe want an answer that says what happened in plain words, and that Rust can check for us.\nQuestion: buy_click_upgrade() returns false. What does the calling code actually know?\nThat the player did not have enough gold\nOnly that the purchase did not happen\nThat the method crashed\nAnswer: 2. Only that the purchase did not happen\nExplanation: false says no, and nothing else. The reason lives in the programmer's memory, not in the code, and a second way to fail would hide behind the same false.\nGive the outcome a name\nRust has a type for \"one of these specific things\":\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nPut it with the structs, after Game. It is a type, like they are. Until something uses it, Rust adds one more never-used warning to the pile.\nenum is short for enumeration: a type whose values are listed out, one by one.\nA PurchaseOutcome is either Purchased or NotEnoughGold.\nIt cannot be anything else.\nIt cannot be both.\nThe two options are called variants, and you name one the same way you reach into a type for a function:\nPurchaseOutcome::NotEnoughGold\nYou can think about bool the same way for this purpose: it also stands for one of exactly two possibilities, true or false.\nThe difference is that our enum lets us give those possibilities names that belong to our game.\nThe line above the enum\n#[derive(Debug, PartialEq)]\nPartialEq is what lets == compare two outcomes. Without it, Rust has no idea whether Purchased == Purchased.\nDebug is what lets Rust print an outcome for a programmer to read, so a failing test can show NotEnoughGold in its message.\nNumbers like u32 come with both abilities built in. Our brand-new enum starts with neither.\nRust calls abilities like these traits. That is all you need about traits today: a trait is a named ability a type can have, like \"can be compared with ==.\"\n#[derive(...)] is the one-line way to ask Rust for the standard version of a trait, instead of writing it by hand.\nOur tests will need both.\nUse it\nNow the purchase can say what happened:\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\nSame shape. Same early return.\nOnly the answers changed, and now they read like the game talking.\nGame::buy_click_upgrade() changes its return type to match:\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\nmatch: one branch per answer\nRust's match expression gives us a way to handle each possible variant explicitly.\nHere is how main() will react to a purchase:\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\"Bought it!\"),\nPurchaseOutcome::NotEnoughGold => {}\n}\nmatch looks at the value and runs the branch that fits.\nEach line is called an arm: a pattern, an arrow, and what to do.\nThat arrow is =>, a fat arrow. It is not the -> from function signatures, and your fingers will try to type that one at least once.\nArms end with a comma, like items in a list. An arm whose body is a block in braces can leave the comma off.\nThe {} on the second branch means: do nothing.\nThat is allowed. What is not allowed is leaving a variant out.\nDelete the NotEnoughGold line and Rust refuses to compile:\nerror[E0004]: non-exhaustive patterns:\n`PurchaseOutcome::NotEnoughGold` not covered\nThis is the payoff.\nWith our bool version, the caller had to remember what false meant.\nWith an enum and match, the possible outcomes are named, and Rust can make us handle every one.\nIf we add a third variant next month, every match that names the variants will stop compiling until it says what to do about it.\nRust is keeping a list for us.\nThe full error message also offers to fill the gap for you, with a wildcard pattern or a todo!() placeholder that crashes if it ever runs. A wildcard (_ =>, meaning \"anything else\") would compile, but it switches this check off for every variant you add later. Naming each variant is what keeps the list.\nTry it. Run this, then delete one arm and run it again. Then put the arm back, add a third variant to the enum, and run it without touching the match.\nLive example · break the match on purpose\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nfn try_to_buy(gold: u32, cost: u32) -> PurchaseOutcome {\nif gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nPurchaseOutcome::Purchased\n}\nfn main() {\nfor gold in [5, 10, 15] {\nmatch try_to_buy(gold, 10) {\nPurchaseOutcome::Purchased => println!(\"{gold} gold: bought it!\"),\nPurchaseOutcome::NotEnoughGold => println!(\"{gold} gold: not enough.\"),\n}\n}\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nQuestion: You add a MaxLevelReached variant to PurchaseOutcome. What does Rust do the next time you build?\nNothing, until a purchase actually returns MaxLevelReached\nIt refuses to compile the enum itself\nEvery match that names the variants, but not the new one, stops compiling\nEvery match quietly skips the new variant\nAnswer: 3. Every match that names the variants, but not the new one, stops compiling\nExplanation: The check happens when the program is built, not when it runs. The match in main() gets an E0004 error until it handles the new variant.\nQuestion: Rust reports E0004 for a missing arm, and you silence it by adding _ => {}. What have you given up?\nThe existing arms stop running\nRust will no longer tell you when a variant added later needs handling\nThe match no longer compiles\nAnswer: 2. Rust will no longer tell you when a variant added later needs handling\nExplanation: _ means \"anything else\", so a future variant lands in it silently. Naming every variant is what keeps Rust's list.\nmain() goes shopping\nAdd the purchase to the loop, after the stage-cleared block:\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\nEvery turn, main() tries to buy an upgrade, and only says something when it works.\nWorth being clear about what that means: in this terminal version, the game buys an upgrade the moment the player can afford one. Nobody is deciding anything.\nLater, when this game has a button, the player will choose when to spend. The purchasing rule does not change when that happens. Only who calls it.\nRun it:\ncargo run\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nBought click upgrade 1! Click damage: 7  Gold left: 2\nStage 3 cleared!  Gold: 11  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 22  XP: 16/35\nNext up: Training Slime with 60 health.\nBought click upgrade 2! Click damage: 10  Gold left: 2\nStage 5 cleared!  Gold: 15  XP: 30/35\nNext up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 30  XP: 11/50\nLevel up! Now level 3. Click damage: 11\nNext up: Training Slime with 80 health.\nBought click upgrade 3! Click damage: 13  Gold left: 0\nStage 7 cleared!  Gold: 17  XP: 29/50\nNext up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 36  XP: 49/50\nNext up: Training Slime with 100 health.\nNow gold goes somewhere.\nIt climbs, and then it drops to almost nothing, because something got bought.\nClick damage goes 5 → 7 → 8 → 10 → 11 → 13, pushed up from two directions at once: levels the player earns, and upgrades the player buys.\nCompare the last line to where we started this article. Ninety-six gold and nothing to do with it, versus thirty-six gold and three upgrades already paid for.\nTesting the purchase\nThe pricing rule gets a test for its numbers. Put the new tests in mod tests, after levels_make_the_player_hit_harder:\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\nThen the two things that can happen.\nA broke player cannot buy anything, and a failed purchase changes nothing:\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\nThose assert_eq! lines comparing outcomes are exactly why we asked for PartialEq and Debug.\nassert_eq! needs both. PartialEq does the comparison. Debug prints the two sides if they do not match. Leave either one out of the derive line and these tests will not even compile.\ncargo run would still build without them, because match needs neither. It is the tests that ask.\nQuestion: You delete Debug from #[derive(Debug, PartialEq)]. What happens to the test that runs assert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased)?\nIt compiles, but a failing test prints an empty message\nThe test code no longer compiles\nNothing changes\nIt panics when the test runs\nAnswer: 2. The test code no longer compiles\nExplanation: assert_eq! has to be able to print both sides in case they differ, so it needs Debug before the test can even be built.\nRun them:\ncargo test\nAll sixteen should pass.\nThe complete program\nIf you are only reading along, skip ahead to the coding challenge (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#your-turn-cap-the-upgrades).\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\n<details>\n<summary><code>src/main.rs</code> · complete 339-line program</summary>\nstruct Player {\ngold: u32,\nxp: u32,\nlevel: u32,\nclick_upgrade_level: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\n#[derive(Debug, PartialEq)]\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\ngold: 0,\nxp: 0,\nlevel: 1,\nclick_upgrade_level: 0,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nfn bonus_damage(&self) -> u32 {\nclick_upgrade_bonus(self.click_upgrade_level)\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\nself.player.buy_click_upgrade()\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn click_upgrade_bonus(upgrades: u32) -> u32 {\nupgrades.saturating_mul(2)\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage());\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage())\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\nmatch game.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought click upgrade {}! Click damage: {}  Gold left: {}\",\ngame.player.click_upgrade_level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage()),\ngame.player.gold\n),\nPurchaseOutcome::NotEnoughGold => {}\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 5);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn upgrades_cost_more_each_time() {\nassert_eq!(click_upgrade_cost(0), 10);\nassert_eq!(click_upgrade_cost(1), 20);\n}\n#[test]\nfn buying_an_upgrade_needs_gold() {\nlet mut player = Player::new();\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::NotEnoughGold);\nassert_eq!(player.click_upgrade_level, 0);\nassert_eq!(player.bonus_damage(), 0);\n}\n#[test]\nfn buying_an_upgrade_spends_gold() {\nlet mut player = Player::new();\nplayer.gold = 12;\nassert_eq!(player.buy_click_upgrade(), PurchaseOutcome::Purchased);\nassert_eq!(player.gold, 2);\nassert_eq!(player.click_upgrade_level, 1);\nassert_eq!(player.bonus_damage(), 2);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 15);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\n</details>\nRun both:\ncargo run\ncargo test\nThe game should clear eight stages, buying three upgrades along the way, and all sixteen tests should pass.\nYour turn: cap the upgrades\nA third way for a purchase to fail is exactly what the enum was built for. This is your chance to watch Rust walk you to every place that needs to know.\nCoding challenge: a maxed-out upgrade\nStop selling click upgrades after the fifth one.\nAdd a third variant to PurchaseOutcome, for an upgrade that is maxed out.\nIn buy_click_upgrade(), return that variant when the player already has 5 upgrades. Check this before the price.\nPress Run. Rust will refuse to compile until the match in main() handles your new variant. Add that arm.\nKeep Player, its two fields and buy_click_upgrade() as they are, because the checks call them. Check my code will tell you how you are doing.\nExpected result: five \"Bought upgrade\" lines, then two lines saying the upgrades are maxed out. Gold left stops at 850.\nCoding challenge · cap the upgrades\nstruct Player {\ngold: u32,\nclick_upgrade_level: u32,\n}\nenum PurchaseOutcome {\nPurchased,\nNotEnoughGold,\n// TODO 1: add a third variant for \"this upgrade is maxed out\"\n}\nimpl Player {\nfn buy_click_upgrade(&mut self) -> PurchaseOutcome {\n// TODO 2: stop at 5 upgrades, before checking the price\nlet cost = click_upgrade_cost(self.click_upgrade_level);\nif self.gold < cost {\nreturn PurchaseOutcome::NotEnoughGold;\n}\nself.gold -= cost;\nself.click_upgrade_level = self.click_upgrade_level.saturating_add(1);\nPurchaseOutcome::Purchased\n}\n}\nfn click_upgrade_cost(upgrades: u32) -> u32 {\nupgrades.saturating_add(1).saturating_mul(10)\n}\nfn main() {\nlet mut player = Player {\ngold: 1000,\nclick_upgrade_level: 0,\n};\nfor _ in 0..7 {\nmatch player.buy_click_upgrade() {\nPurchaseOutcome::Purchased => println!(\n\"Bought upgrade {}. Gold left: {}\",\nplayer.click_upgrade_level, player.gold\n),\nPurchaseOutcome::NotEnoughGold => println!(\"Not enough gold.\"),\n// TODO 3: tell the player the upgrade is maxed out\n}\n}\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// Once a player holds 5 upgrades, buying again should leave their gold and level alone. Look at what your function does at the cap.\n#[test]\nfn sixth_upgrade_is_refused() {\nlet mut player = Player {\ngold: 1000,\nclick_upgrade_level: 5,\n};\nplayer.buy_click_upgrade();\nassert_eq!(player.gold, 1000);\nassert_eq!(player.click_upgrade_level, 5);\n}\n/// A player at the cap with an empty purse is maxed out, not broke. Which check comes first?\n#[test]\nfn cap_is_checked_before_price() {\nlet mut player = Player {\ngold: 0,\nclick_upgrade_level: 5,\n};\nlet outcome = player.buy_click_upgrade();\nassert!(!matches!(outcome, PurchaseOutcome::NotEnoughGold));\nassert!(!matches!(outcome, PurchaseOutcome::Purchased));\n}\n/// The fifth upgrade still has to sell. Look at where your cap starts.\n#[test]\nfn fifth_upgrade_still_sells() {\nlet mut player = Player {\ngold: 1000,\nclick_upgrade_level: 4,\n};\nlet outcome = player.buy_click_upgrade();\nassert!(matches!(outcome, PurchaseOutcome::Purchased));\nassert_eq!(player.click_upgrade_level, 5);\nassert_eq!(player.gold, 950);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/).\nTip:\nStep 2 is an early return, just like the NotEnoughGold check below it. Compare self.click_upgrade_level with 5 using >=.\n<details>\n<summary>Show one solution</summary>\nThe new variant:\nMaxedOut,\nThe early return, at the top of buy_click_upgrade():\nif self.click_upgrade_level >= 5 {\nreturn PurchaseOutcome::MaxedOut;\n}\nThe new arm in main():\nPurchaseOutcome::MaxedOut => println!(\"Click upgrades are maxed out.\"),\n</details>\nThe challenge is its own small program, so your src/main.rs stays exactly as it is above.\nWhat we learned\nAlong the way, we encountered:\na second rule-derived value, replacing a stored one\nearly return\nenum and variants\ntraits, in one sentence, and #[derive(Debug, PartialEq)]\nmatch, and the compiler refusing to let us forget a case\nTwo ideas are worth keeping.\nThe first finishes the job base_damage() started. Player has no damage numbers in it at all now. It stores progress, and damage is a rule applied to that progress.\nThe second is PurchaseOutcome.\nWhen a bool would have made the caller remember what false meant, we gave the answers names instead, and Rust started checking that every answer gets handled.\nThat pattern shows up everywhere in Rust. Two common examples you will eventually meet are Option, for a value that might not be there, and Result, for an operation that might have failed.\nThis is the first time we needed our own. It will not be the last.\nNothing happens unless you click\nOur game finally has a progression loop.\nKill a slime, get paid, level up, buy damage, kill a tougher slime.\nThere is just one problem with calling it an idle game.\nclicks per second while you are playing: a lot\nclicks per second while you are not:    zero\nNothing happens unless you click.\nWalk away, come back an hour later, and the slime is exactly where you left it.\nAn idle game is supposed to keep working while you do not.\nNext time, we are going to give the game time.\nNot a timer. Not a clock. Just a number: how many seconds went by.\nThe game will take that number and work out everything that should have happened.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-21T04:15:10Z",
      "id": "https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/",
      "image": "https://mnixblog.netlify.app/images/rust-enums-and-match-click-upgrades/cover.jpg",
      "summary": "Our player has ninety-six gold and nothing to buy. Build a shop in Rust, then replace a vague true or false with an enum and let match handle every outcome.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "False Is Not an Answer.",
      "url": "https://mnixblog.netlify.app/rust-enums-and-match-click-upgrades/"
    },
    {
      "content_html": "<p>Our game can fight its way through stage after stage now.</p>\n<p>Click. The slime falls. A tougher one oozes in. Click.</p>\n<p>That is a loop.</p>\n<p>It is also a treadmill.</p>\n<p>Every slime gets stronger. Our player never does.</p>\n<p>In this article, winning starts to pay: gold and experience for every kill, and levels that make the player hit harder.</p>\n<p>By the end, clearing a stage prints this:</p>\n<pre><code class=\"language-text\">Stage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\n</code></pre>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, you can skip past this code to \"Where does the gold go?\"</p>\n<p>If you are building along, you need three <code>struct</code>s, two <code>impl</code> blocks, five small functions, and eight tests:</p>\n<details>\n<summary><code>src/main.rs</code> · 160-line starting point</summary>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\"Stage {stage} cleared!\");\n        } else {\n            println!(\"{} health: {}\", game.enemy.name, game.enemy.health);\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player {\n            base_damage: 5,\n            bonus_damage: 2,\n        };\n\n        let mut enemy = Enemy {\n            name: String::from(\"Training Slime\"),\n            health: 10,\n        };\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_advances_the_stage() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n    }\n}\n</code></pre>\n</details>\n<p><code>game.click()</code> attacks the enemy. If that attack kills it, <code>defeat_enemy()</code> advances the stage and sends in the next one.</p>\n<p>The tests hold those rules in place. Three of them are about to change, so it helps to see them as they are now.</p>\n<p>Everything today builds on that.</p>\n<hr />\n<h2 id=\"where-does-the-gold-go\">Where does the gold go?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#where-does-the-gold-go\" aria-label=\"Link to this section\">#</a></h2>\n<p>Defeating an enemy should pay.</p>\n<p>Before we can pay the player, the player needs somewhere to put the money.</p>\n<p>Right now, our <code>Player</code> has two fields, and neither of them is a wallet.</p>\n<p>Add one:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n    gold: u32,\n}\n</code></pre>\n<p>Rust immediately complains.</p>\n<p>Every place we build a <code>Player</code> now has to say how much gold it starts with.</p>\n<p>There are two of those: <code>Game::new()</code> and the <code>attack_reduces_enemy_health</code> test. (<code>cargo run</code> only finds the first one, because it does not compile the tests. <code>cargo test</code> finds both.)</p>\n<p>We are about to change <code>Player</code> a few more times today, and we do not want to chase every field through every place a player gets built.</p>\n<p><code>Enemy::for_stage()</code> already solves this exact problem for enemies.</p>\n<p>Give <code>Player</code> the same treatment:</p>\n<pre><code class=\"language-rust\">impl Player {\n    fn new() -&gt; Player {\n        Player {\n            base_damage: 5,\n            bonus_damage: 2,\n            gold: 0,\n        }\n    }\n}\n</code></pre>\n<p>Put it right after the structs, next to <code>impl Enemy</code>.</p>\n<p>Same convention as <code>Game::new()</code>: <code>new</code> is not a Rust keyword, just the name Rust programmers use for \"make me a fresh one of these.\"</p>\n<p>Then use it in <code>Game::new()</code>:</p>\n<pre><code class=\"language-rust\">            player: Player::new(),\n</code></pre>\n<p>and in the test:</p>\n<pre><code class=\"language-rust\">        let player = Player::new();\n</code></pre>\n<p>Now there is exactly one place that knows what a new player looks like.</p>\n<p>When <code>Player</code> changes again, and it will, that is the only place we touch.</p>\n<p>While you are in the tests, <code>a_new_game_starts_at_stage_one</code> can check that the wallet starts empty:</p>\n<pre><code class=\"language-rust\">    assert_eq!(game.player.gold, 0);\n</code></pre>\n<p>Run <code>cargo test</code>.</p>\n<p>All eight should still pass.</p>\n<p>If you <code>cargo run</code> instead, Rust warns that <code>gold</code> is never read. A warning, not an error. The game is about to read it.</p>\n<hr />\n<h2 id=\"how-much-is-a-slime-worth\">How much is a slime worth?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#how-much-is-a-slime-worth\" aria-label=\"Link to this section\">#</a></h2>\n<p>How much gold should a defeated enemy drop?</p>\n<p>That is a game rule.</p>\n<p>Game rules get their own small function, like <code>enemy_health_for_stage()</code>.</p>\n<p>Give this one the same treatment, right next to it:</p>\n<pre><code class=\"language-rust\">fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n</code></pre>\n<pre><code class=\"language-text\">stage 1 → 5 gold\nstage 2 → 7 gold\nstage 3 → 9 gold\n</code></pre>\n<p>Try it: press Run to see those three numbers, plus two more stages. The last line asks for the reward at the biggest stage a <code>u32</code> can hold. Rewrite the body as <code>stage * 2 + 3</code> and run it again. In this debug build, Rust stops with an \"attempt to multiply with overflow\" panic instead of printing a number.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · saturating or not</figcaption><pre><code class=\"language-rust\">fn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn main() {\n    for stage in 1..=5 {\n        println!(&quot;stage {stage} -&gt; {} gold&quot;, gold_reward_for_stage(stage));\n    }\n\n    println!(&quot;stage {} -&gt; {} gold&quot;, u32::MAX, gold_reward_for_stage(u32::MAX));\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\">the original article</a>.</p></figure>\n<p>As written, the last line prints <code>4294967295 gold</code>: the biggest <code>u32</code>, because the arithmetic stops at the top instead of going past it.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>gold_reward_for_stage()</code> use <code>saturating_mul</code> and <code>saturating_add</code> instead of <code>*</code> and <code>+</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>They make the reward bigger at high stages</li>\n<li>If the result would be too big for a <code>u32</code>, they stop at the largest value instead of overflowing</li>\n<li>Plain <code>*</code> and <code>+</code> do not work on <code>u32</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. If the result would be too big for a <code>u32</code>, they stop at the largest value instead of overflowing</p>\n<p>Plain <code>*</code> and <code>+</code> work, but an overflow panics in a debug build and wraps around in a release build. Saturating arithmetic gives the same safe answer in both.</p>\n</div>\n</div>\n</section>\n<p>Later enemies are tougher.</p>\n<p>They should also pay better.</p>\n<p>Now, where does that reward live?</p>\n<p>It belongs to the enemy.</p>\n<p>A slime knows how much health it has.</p>\n<p>It should also know what it is worth:</p>\n<pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n}\n</code></pre>\n<p>And <code>Enemy::for_stage()</code> fills it in:</p>\n<pre><code class=\"language-rust\">impl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<p>Because every enemy is built through <code>for_stage()</code>, this is the only place that needed to change.</p>\n<p>Now run <code>cargo test</code> again.</p>\n<p><code>cargo run</code> would build, with one more never-read warning, because the game only builds enemies through <code>for_stage()</code>.</p>\n<p>The tests do not build. <code>attack_reduces_enemy_health</code> builds an <code>Enemy</code> by hand, with a name and 10 health, and Rust wants a <code>gold_reward</code> there too.</p>\n<p>That is the compiler finding the last place for us.</p>\n<p>Instead of making that test learn about rewards, let it start from a real enemy and adjust the one thing it cares about:</p>\n<pre><code class=\"language-rust\">        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n</code></pre>\n<p>The test still checks what it always checked: an attack takes health off the enemy.</p>\n<hr />\n<h2 id=\"paying-out\">Paying out<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#paying-out\" aria-label=\"Link to this section\">#</a></h2>\n<p>The player has a wallet. The enemy has a price on its head.</p>\n<p>Handing one to the other is the player's job.</p>\n<p>Add a method to <code>impl Player</code>, under <code>new()</code>:</p>\n<pre><code class=\"language-rust\">    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n</code></pre>\n<p><code>&amp;mut self</code> is the same <code>&amp;mut self</code> from <code>click()</code>.</p>\n<p>The method borrows the player, changes it, and gives it back.</p>\n<p>Which brings us to <code>defeat_enemy()</code>, the one place in the game that already knows an enemy just died.</p>\n<p>It grows:</p>\n<pre><code class=\"language-rust\">    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n</code></pre>\n<p>The order matters.</p>\n<p>We pay the player <strong>before</strong> the enemy is replaced.</p>\n<p>On the next line, <code>self.enemy</code> becomes a brand new slime with a brand new reward.</p>\n<p>If we paid afterward, the player would get paid for an enemy they have not fought yet.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In <code>defeat_enemy()</code>, why does <code>add_gold()</code> come before <code>self.enemy = Enemy::for_stage(self.stage)</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust requires method calls to come before assignments</li>\n<li>After the assignment, <code>self.enemy.gold_reward</code> belongs to the next enemy, not the one just defeated</li>\n<li><code>add_gold()</code> would not compile after the enemy is replaced</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. After the assignment, <code>self.enemy.gold_reward</code> belongs to the next enemy, not the one just defeated</p>\n<p>The code would still compile. It would just pay the wrong reward, which is why the order is a game rule and not something Rust can catch.</p>\n</div>\n</div>\n</section>\n<p>Read <code>defeat_enemy()</code> out loud now:</p>\n<blockquote>\n<p>Pay the player. Go to the next stage. Bring in the next enemy.</p>\n</blockquote>\n<p>No arithmetic to decode.</p>\n<hr />\n<h2 id=\"experience-and-levels\">Experience and levels<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#experience-and-levels\" aria-label=\"Link to this section\">#</a></h2>\n<p>Gold is something to spend.</p>\n<p>Experience, or XP, is something to keep.</p>\n<p>Every defeated enemy should also give the player experience, and when they have enough, they level up.</p>\n<p>That is two rules, so we need two numbers:</p>\n<pre><code class=\"language-text\">how much XP does an enemy give?\nhow much XP does the next level take?\n</code></pre>\n<p>The first one looks like the gold rule.</p>\n<p>Put it right under <code>gold_reward_for_stage()</code>:</p>\n<pre><code class=\"language-rust\">fn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n</code></pre>\n<p>The second one is new.</p>\n<p>Each level should take a little more than the last:</p>\n<pre><code class=\"language-rust\">fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n</code></pre>\n<pre><code class=\"language-text\">level 1 → 20 XP to reach level 2\nlevel 2 → 35 XP to reach level 3\nlevel 3 → 50 XP to reach level 4\n</code></pre>\n<p>That chain is split across lines so each step is easy to read. A line starting with <code>.</code> just continues the line above it.</p>\n<p>Level 1 is the starting line. The <code>saturating_sub(1)</code> makes it count as zero steps taken, so level 1 needs the plain 20 and every level after that adds 15. Without it, the first level would already cost 35.</p>\n<p>The exact numbers do not matter yet.</p>\n<p>They have names and they will have tests.</p>\n<p>The enemy carries its XP reward the same way it carries gold:</p>\n<pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n</code></pre>\n<p>Add it to <code>Enemy::for_stage()</code>, under <code>gold_reward</code>:</p>\n<pre><code class=\"language-rust\">            xp_reward: xp_reward_for_stage(stage),\n</code></pre>\n<p>And the player needs two more fields: how much XP they have, and what level they are:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n</code></pre>\n<p>In <code>Player::new()</code>:</p>\n<pre><code class=\"language-rust\">            xp: 0,\n            level: 1,\n</code></pre>\n<p>That is now the normal place a fresh player is built, so future changes stay concentrated there.</p>\n<p>Run <code>cargo test</code>. Still eight, plus a few warnings that the new fields and <code>xp_to_next_level()</code> are never used. The next section uses them.</p>\n<hr />\n<h2 id=\"leveling-up\">Leveling up<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#leveling-up\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now the interesting part.</p>\n<p>Adding XP is not like adding gold.</p>\n<p>Gold just piles up.</p>\n<p>XP piles up until it crosses a line, and then something happens:</p>\n<pre><code class=\"language-rust\">    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n</code></pre>\n<p>Add it to <code>impl Player</code>, right under <code>add_gold()</code>.</p>\n<p>Walk through it with a level 1 player who has 0 XP and just earned 25.</p>\n<p>The XP becomes 25.</p>\n<p>Level 1 needs 20. We have 25. That is enough, so we go into the loop.</p>\n<p>Spend 20 of it. XP is now 5. Level is now 2.</p>\n<p>Level 2 needs 35. We have 5. The loop stops.</p>\n<p>The player is level 2 with 5 XP toward the next one.</p>\n<p>Why a <code>while</code> and not an <code>if</code>?</p>\n<p>Because a reward might be big enough to cross two lines at once.</p>\n<p>Give that same level 1 player 60 XP.</p>\n<p>Spend 20 → level 2, 40 left.</p>\n<p>Level 2 needs 35. 40 is enough. Spend 35 → level 3, 5 left.</p>\n<p>Level 3 needs 50. The loop stops.</p>\n<p>An <code>if</code> would have stopped at level 2 and left 40 XP sitting there, more than enough for another level the player never got.</p>\n<p>A <code>while</code> keeps going until the XP really is below the line.</p>\n<p>Try it: press Run and the 60 XP walkthrough plays out as <code>level 3 with 5 XP left over</code>. Then change <code>while</code> to <code>if</code> and run it again. The output becomes <code>level 2 with 40 XP left over</code>, which is the stuck player from above.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · swap the while for an if</figcaption><pre><code class=\"language-rust\">struct Player {\n    xp: u32,\n    level: u32,\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nimpl Player {\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n}\n\nfn main() {\n    let mut player = Player { xp: 0, level: 1 };\n\n    player.add_xp(60);\n\n    println!(&quot;level {} with {} XP left over&quot;, player.level, player.xp);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A level 1 player earns 60 XP at once. What does <code>if</code> instead of <code>while</code> leave behind?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A player at level 3 with 5 XP</li>\n<li>A player at level 2 holding 40 XP, enough for another level they never got</li>\n<li>A compile error, because <code>if</code> cannot be used there</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. A player at level 2 holding 40 XP, enough for another level they never got</p>\n<p><code>if</code> checks the line once. <code>while</code> keeps checking until the XP really is below it.</p>\n</div>\n</div>\n</section>\n<p>You might notice that <code>xp_to_next_level(self.level)</code> gets called twice in a row. That is fine. It is a tiny calculation, and for the same level it always gives the same answer.</p>\n<p>That matters more than it looks right now. Later in this series, a player is going to come back after a few hours away and collect a very large pile of XP all at once.</p>\n<h3 id=\"a-plain-minus-sign\">A plain minus sign<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#a-plain-minus-sign\" aria-label=\"Link to this section\">#</a></h3>\n<p>Look at this line:</p>\n<pre><code class=\"language-rust\">self.xp -= xp_to_next_level(self.level);\n</code></pre>\n<p><code>-=</code> is shorthand: <code>self.xp -= 20</code> means <code>self.xp = self.xp - 20</code>.</p>\n<p>That is ordinary subtraction, not <code>saturating_sub()</code>.</p>\n<p>Everywhere else in this program, subtraction goes through <code>saturating_sub()</code>.</p>\n<p>Here, it is safe, and the line right above it is the reason.</p>\n<p>The <code>while</code> only lets us in when <code>self.xp</code> is at least as big as the amount we are about to subtract.</p>\n<p>The result can never drop below zero.</p>\n<p><code>saturating_sub()</code> is for when a number <em>might</em> go somewhere it should not.</p>\n<p>When the code right next to it proves that it cannot, plain subtraction says exactly what it means.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why is the plain <code>-=</code> in <code>add_xp()</code> safe?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>-=</code> quietly stops at zero, the same as <code>saturating_sub()</code></li>\n<li>The <code>while</code> condition only lets us in when <code>self.xp</code> is at least the amount being subtracted</li>\n<li><code>xp</code> is a <code>u32</code>, and <code>u32</code> can never go below zero</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The <code>while</code> condition only lets us in when <code>self.xp</code> is at least the amount being subtracted</p>\n<p>A <code>u32</code> cannot hold a negative number, so a plain subtraction that went below zero would be a bug, not a clamp. Here the loop condition rules that out.</p>\n</div>\n</div>\n</section>\n<p>Now <code>defeat_enemy()</code> pays both:</p>\n<pre><code class=\"language-rust\">        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n</code></pre>\n<hr />\n<h2 id=\"what-does-a-level-actually-do\">What does a level actually do?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#what-does-a-level-actually-do\" aria-label=\"Link to this section\">#</a></h2>\n<p>Right now, nothing.</p>\n<p>The player levels up, and hits exactly as hard as before.</p>\n<p>A level should make the player stronger.</p>\n<p>Look at <code>Player</code> again:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n</code></pre>\n<p><code>base_damage</code> is a number we wrote down once, when the player was created, and never touched again.</p>\n<p>But base damage should not be a fact about the player anymore.</p>\n<p>It should be a <em>consequence</em> of the player's level.</p>\n<p>That makes it a rule. Put it under <code>xp_to_next_level()</code>:</p>\n<pre><code class=\"language-rust\">fn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n</code></pre>\n<pre><code class=\"language-text\">level 1 → 5 damage\nlevel 2 → 6 damage\nlevel 3 → 7 damage\n</code></pre>\n<p>Level 1 gives 5, which is exactly the <code>base_damage</code> we have had since the beginning.</p>\n<p>Nothing about the first fight changes.</p>\n<p>Now the field can go.</p>\n<p>Remove <code>base_damage</code> from the struct and from <code>Player::new()</code>.</p>\n<p>Rust will complain about one more place that still uses it. We will get to that in a second.</p>\n<p>In its place, give <code>Player</code> a method that asks the rule, right under <code>add_xp()</code>:</p>\n<pre><code class=\"language-rust\">    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n</code></pre>\n<p>There is something new in that signature.</p>\n<p><code>&amp;self</code>, without the <code>mut</code>.</p>\n<p>So far, our methods have needed <code>&amp;mut self</code> because they changed something.</p>\n<p>A method that only needs to <strong>read</strong> takes <code>&amp;self</code> instead. This is our first one.</p>\n<p><code>base_damage()</code> looks at the player's level and reports a number.</p>\n<p>It does not change anything, so it does not ask for permission to.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>base_damage()</code> take <code>&amp;self</code> and <code>add_xp()</code> take <code>&amp;mut self</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>&amp;self</code> methods run faster</li>\n<li><code>add_xp()</code> changes the player, and <code>base_damage()</code> only reads it</li>\n<li>Only the first method in an <code>impl</code> block may change the player</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>add_xp()</code> changes the player, and <code>base_damage()</code> only reads it</p>\n<p><code>&amp;mut self</code> is the borrow that allows changes. A method that only reports a value asks for the smaller <code>&amp;self</code>.</p>\n</div>\n</div>\n</section>\n<p>One thing still uses the old field: <code>attack()</code>.</p>\n<p>It reads <code>player.base_damage</code>, and that field no longer exists.</p>\n<p>Rust says so (the line is wrapped here to fit):</p>\n<pre><code class=\"language-text\">error[E0615]: attempted to take value of method\n`base_damage` on type `&amp;Player`\n</code></pre>\n<p>and even suggests the fix: use parentheses to call the method.</p>\n<pre><code class=\"language-rust\">fn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p>The parentheses are the only difference.</p>\n<p><code>player.base_damage</code> was a number stored in the player.</p>\n<p><code>player.base_damage()</code> is a number the player works out from its level.</p>\n<p><code>attack()</code> did not have to learn anything about levels.</p>\n<p>It asked for base damage. It got base damage.</p>\n<p>Run <code>cargo test</code>. All eight still pass, and that is the point: a level 1 player still deals 5 base damage, so nothing a test checks has changed.</p>\n<hr />\n<h2 id=\"main-shows-the-progress\"><code>main()</code> shows the progress<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#main-shows-the-progress\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our <code>main()</code> has been printing the enemy's health after every click.</p>\n<p>That was useful when a fight took three clicks. Now there are rewards to watch, so let us print when something happens instead:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage)\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n    }\n}\n</code></pre>\n<p>Before each click, <code>main()</code> remembers the stage and the level.</p>\n<p>After the click, it compares.</p>\n<p>Stage went up? The enemy died, so say so, show the wallet, and introduce the next one.</p>\n<p>Level went up? Say that too.</p>\n<p>Notice that the level check sits <em>inside</em> the stage check. It can, because XP is only ever handed out in <code>defeat_enemy()</code>. No enemy died, no XP, no level up.</p>\n<p>One small thing about that first <code>println!</code>. <code>{stage}</code> works because <code>stage</code> is a plain variable. <code>game.player.gold</code> has dots in it, so it goes in the list after the string and a bare <code>{}</code> picks it up.</p>\n<p><code>main()</code> still does not know how leveling works.</p>\n<p>It does borrow one rule, <code>xp_to_next_level()</code>, so it can show how far away the next level is. That is display, not a decision.</p>\n<p>It clicks, it asks, and it prints what it was told.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<pre><code class=\"language-text\">Stage 1 cleared!  Gold: 5  XP: 6/20\n  Next up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\n  Next up: Training Slime with 40 health.\nStage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\n  Next up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 32  XP: 16/35\n  Next up: Training Slime with 60 health.\nStage 5 cleared!  Gold: 45  XP: 30/35\n  Next up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 60  XP: 11/50\nLevel up! Now level 3. Click damage: 9\n  Next up: Training Slime with 80 health.\nStage 7 cleared!  Gold: 77  XP: 29/50\n  Next up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 96  XP: 49/50\n  Next up: Training Slime with 100 health.\n</code></pre>\n<p>Read that top to bottom.</p>\n<p>Gold climbs. XP climbs toward a line, crosses it, and resets with the leftover carried over. Click damage starts at 7 and climbs to 8, then 9.</p>\n<p>Compare that to where we started this article: 5 base damage plus 2 bonus, on stage 1 and on stage 30 alike.</p>\n<p>The slimes still get tougher every stage. The difference is that now, so does the player.</p>\n<hr />\n<h2 id=\"testing-the-rules\">Testing the rules<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#testing-the-rules\" aria-label=\"Link to this section\">#</a></h2>\n<p>Each new rule gets a test for its numbers. Add these to the <code>tests</code> module:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n</code></pre>\n<p>Then the behaviors.</p>\n<p>The two walkthroughs from <code>add_xp()</code> become tests, so the explanation and the code can never drift apart:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n</code></pre>\n<p>And the big one.</p>\n<p>The last test in our starting point, <code>defeating_an_enemy_advances_the_stage</code>, checked that a win moved the game forward.</p>\n<p>Now a win should also pay.</p>\n<p>Rename it and add two checks at the end:</p>\n<pre><code class=\"language-rust\">    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n</code></pre>\n<p>This test does not care how damage is calculated, or how XP crosses a threshold.</p>\n<p>Those rules have their own tests now.</p>\n<p>It asks the bigger question:</p>\n<blockquote>\n<p>When a click kills something, does the player get paid for it?</p>\n</blockquote>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All thirteen should pass.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, skip to the last two sections.</p>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<details>\n<summary><code>src/main.rs</code> · complete 268-line program</summary>\n<pre><code class=\"language-rust\">struct Player {\n    bonus_damage: u32,\n    gold: u32,\n    xp: u32,\n    level: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n    gold_reward: u32,\n    xp_reward: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Player {\n    fn new() -&gt; Player {\n        Player {\n            bonus_damage: 2,\n            gold: 0,\n            xp: 0,\n            level: 1,\n        }\n    }\n\n    fn add_gold(&amp;mut self, amount: u32) {\n        self.gold = self.gold.saturating_add(amount);\n    }\n\n    fn add_xp(&amp;mut self, amount: u32) {\n        self.xp = self.xp.saturating_add(amount);\n\n        while self.xp &gt;= xp_to_next_level(self.level) {\n            self.xp -= xp_to_next_level(self.level);\n            self.level = self.level.saturating_add(1);\n        }\n    }\n\n    fn base_damage(&amp;self) -&gt; u32 {\n        base_damage_for_level(self.level)\n    }\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n            gold_reward: gold_reward_for_stage(stage),\n            xp_reward: xp_reward_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player::new(),\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.player.add_gold(self.enemy.gold_reward);\n        self.player.add_xp(self.enemy.xp_reward);\n\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn gold_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(3)\n}\n\nfn xp_reward_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(2).saturating_add(4)\n}\n\nfn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn base_damage_for_level(level: u32) -&gt; u32 {\n    level.saturating_add(4)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage(), player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 8 {\n        let stage = game.stage;\n        let level = game.player.level;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\n                \"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\n                game.player.gold,\n                game.player.xp,\n                xp_to_next_level(game.player.level)\n            );\n\n            if game.player.level &gt; level {\n                println!(\n                    \"Level up! Now level {}. Click damage: {}\",\n                    game.player.level,\n                    click_damage(game.player.base_damage(), game.player.bonus_damage)\n                );\n            }\n\n            println!(\n                \"  Next up: {} with {} health.\",\n                game.enemy.name, game.enemy.health\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player::new();\n\n        let mut enemy = Enemy::for_stage(1);\n        enemy.health = 10;\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn later_stages_pay_better() {\n        assert_eq!(gold_reward_for_stage(1), 5);\n        assert_eq!(gold_reward_for_stage(2), 7);\n        assert_eq!(xp_reward_for_stage(1), 6);\n        assert_eq!(xp_reward_for_stage(2), 8);\n    }\n\n    #[test]\n    fn levels_need_more_xp_as_they_go() {\n        assert_eq!(xp_to_next_level(1), 20);\n        assert_eq!(xp_to_next_level(2), 35);\n    }\n\n    #[test]\n    fn leveling_up_keeps_leftover_xp() {\n        let mut player = Player::new();\n\n        player.add_xp(25);\n\n        assert_eq!(player.level, 2);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn a_big_reward_can_skip_levels() {\n        let mut player = Player::new();\n\n        player.add_xp(60);\n\n        assert_eq!(player.level, 3);\n        assert_eq!(player.xp, 5);\n    }\n\n    #[test]\n    fn levels_make_the_player_hit_harder() {\n        assert_eq!(base_damage_for_level(1), 5);\n        assert_eq!(base_damage_for_level(2), 6);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n        assert_eq!(game.player.gold, 0);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_pays_the_player() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n        assert_eq!(game.player.gold, 5);\n        assert_eq!(game.player.xp, 6);\n    }\n}\n</code></pre>\n</details>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear eight stages, and all thirteen tests should pass.</p>\n<p>Want to practice the <code>while</code> loop on its own? Here is a small one to finish.</p>\n<p><strong>Coding challenge: total XP to reach a level</strong></p>\n<p>Write <code>total_xp_to_reach(level)</code>: the XP a player has spent in total by the time they arrive at <code>level</code>. Level 1 is where everyone starts, so it costs nothing. Keep the name <code>total_xp_to_reach</code> and its <code>level</code> argument, because the checks call it. Press <strong>Check my code</strong> and it will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> it prints <code>0</code>, <code>20</code>, <code>55</code> and <code>105</code> for levels 1 to 4. Right now it prints <code>0</code> four times.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Coding challenge · total XP to reach a level</figcaption><pre><code class=\"language-rust\">fn xp_to_next_level(level: u32) -&gt; u32 {\n    level\n        .saturating_sub(1)\n        .saturating_mul(15)\n        .saturating_add(20)\n}\n\nfn total_xp_to_reach(level: u32) -&gt; u32 {\n    // TODO: add up xp_to_next_level for level 1, 2, ... up to (not including) `level`\n    0\n}\n\nfn main() {\n    for level in 1..=4 {\n        println!(&quot;level {level}: {} total XP&quot;, total_xp_to_reach(level));\n    }\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// Level 1 costs nothing, and arriving at level 2 costs what level 1 asks for. Does the loop stop before `level`?\n#[test]\nfn first_steps() {\n    assert_eq!(total_xp_to_reach(1), 0);\n    assert_eq!(total_xp_to_reach(2), 20);\n}\n\n/// Every level on the way has to be added, one at a time. The total grows with each step.\n#[test]\nfn adds_every_step() {\n    assert_eq!(total_xp_to_reach(3), 55);\n    assert_eq!(total_xp_to_reach(4), 105);\n}\n\n/// A huge level should not panic or wrap around. The hint names a method that stops at the ceiling.\n#[test]\nfn stops_at_the_ceiling() {\n    assert_eq!(total_xp_to_reach(100_000), u32::MAX);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Keep a running <code>total</code> (start it as <code>let mut total: u32 = 0;</code>) and a <code>current</code> level that starts at 1. While <code>current</code> is below <code>level</code>, add <code>xp_to_next_level(current)</code> to the total with <code>saturating_add</code>, then move <code>current</code> up by one.</p>\n</aside>\n<p>The challenge is its own small program, so your <code>src/main.rs</code> stays exactly as it is above.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>\n<p><code>Player::new()</code>, so a player is built in one place</p>\n</li>\n<li>\n<p><code>&amp;self</code> methods that read without changing</p>\n</li>\n<li>\n<p>a <code>while</code> loop that keeps going until a condition stops being true</p>\n</li>\n<li>\n<p><code>-=</code>, and plain subtraction where the code above it proves the number cannot go below zero</p>\n</li>\n<li>\n<p>rules that calculate a value instead of storing it</p>\n</li>\n</ul>\n<p>That is a short list, and the last item is the one worth keeping.</p>\n<p><code>base_damage</code> used to be a number sitting in the player.</p>\n<p>Now it is a rule applied to the player's level.</p>\n<p>The player got smaller, and the game got bigger.</p>\n<p>Hold onto that idea. We are going to do it again next time, to the other half of that damage calculation.</p>\n<hr />\n<h2 id=\"gold-with-nothing-to-buy\">Gold with nothing to buy<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#gold-with-nothing-to-buy\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our player finally gets stronger by winning.</p>\n<p>There is just one problem.</p>\n<p>Look at the last line of that run again:</p>\n<pre><code class=\"language-text\">Stage 8 cleared!  Gold: 96  XP: 49/50\n</code></pre>\n<p>Ninety-six gold.</p>\n<p>We call it money, but there is nothing in this game to spend it on.</p>\n<pre><code class=\"language-text\">gold earned: 96\ngold spent:  0\n</code></pre>\n<p>Right now it is not a reward.</p>\n<p>It is a high score.</p>\n<p>Next time, we are going to give the player something to buy, and discover that the obvious way to report whether a purchase worked is not good enough.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Winning Is Not Progress.\nCanonical: https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/\nAdd Gold, XP, and Levels to Your Rust Game\nOur game can fight its way through stage after stage now.\nClick. The slime falls. A tougher one oozes in. Click.\nThat is a loop.\nIt is also a treadmill.\nEvery slime gets stronger. Our player never does.\nIn this article, winning starts to pay: gold and experience for every kill, and levels that make the player hit harder.\nBy the end, clearing a stage prints this:\nStage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nOur starting point\nIf you are only reading along, you can skip past this code to \"Where does the gold go?\"\nIf you are building along, you need three structs, two impl blocks, five small functions, and eight tests:\n<details>\n<summary><code>src/main.rs</code> · 160-line starting point</summary>\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\ngame.click();\nif game.stage > stage {\nprintln!(\"Stage {stage} cleared!\");\n} else {\nprintln!(\"{} health: {}\", game.enemy.name, game.enemy.health);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_advances_the_stage() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\n}\n}\n</details>\ngame.click() attacks the enemy. If that attack kills it, defeat_enemy() advances the stage and sends in the next one.\nThe tests hold those rules in place. Three of them are about to change, so it helps to see them as they are now.\nEverything today builds on that.\nWhere does the gold go?\nDefeating an enemy should pay.\nBefore we can pay the player, the player needs somewhere to put the money.\nRight now, our Player has two fields, and neither of them is a wallet.\nAdd one:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\ngold: u32,\n}\nRust immediately complains.\nEvery place we build a Player now has to say how much gold it starts with.\nThere are two of those: Game::new() and the attack_reduces_enemy_health test. (cargo run only finds the first one, because it does not compile the tests. cargo test finds both.)\nWe are about to change Player a few more times today, and we do not want to chase every field through every place a player gets built.\nEnemy::for_stage() already solves this exact problem for enemies.\nGive Player the same treatment:\nimpl Player {\nfn new() -> Player {\nPlayer {\nbase_damage: 5,\nbonus_damage: 2,\ngold: 0,\n}\n}\n}\nPut it right after the structs, next to impl Enemy.\nSame convention as Game::new(): new is not a Rust keyword, just the name Rust programmers use for \"make me a fresh one of these.\"\nThen use it in Game::new():\nplayer: Player::new(),\nand in the test:\nlet player = Player::new();\nNow there is exactly one place that knows what a new player looks like.\nWhen Player changes again, and it will, that is the only place we touch.\nWhile you are in the tests, a_new_game_starts_at_stage_one can check that the wallet starts empty:\nassert_eq!(game.player.gold, 0);\nRun cargo test.\nAll eight should still pass.\nIf you cargo run instead, Rust warns that gold is never read. A warning, not an error. The game is about to read it.\nHow much is a slime worth?\nHow much gold should a defeated enemy drop?\nThat is a game rule.\nGame rules get their own small function, like enemy_health_for_stage().\nGive this one the same treatment, right next to it:\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nstage 1 → 5 gold\nstage 2 → 7 gold\nstage 3 → 9 gold\nTry it: press Run to see those three numbers, plus two more stages. The last line asks for the reward at the biggest stage a u32 can hold. Rewrite the body as stage * 2 + 3 and run it again. In this debug build, Rust stops with an \"attempt to multiply with overflow\" panic instead of printing a number.\nLive example · saturating or not\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn main() {\nfor stage in 1..=5 {\nprintln!(\"stage {stage} -> {} gold\", gold_reward_for_stage(stage));\n}\nprintln!(\"stage {} -> {} gold\", u32::MAX, gold_reward_for_stage(u32::MAX));\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/).\nAs written, the last line prints 4294967295 gold: the biggest u32, because the arithmetic stops at the top instead of going past it.\nQuestion: Why does gold_reward_for_stage() use saturating_mul and saturating_add instead of * and +?\nThey make the reward bigger at high stages\nIf the result would be too big for a u32, they stop at the largest value instead of overflowing\nPlain * and + do not work on u32\nAnswer: 2. If the result would be too big for a u32, they stop at the largest value instead of overflowing\nExplanation: Plain * and + work, but an overflow panics in a debug build and wraps around in a release build. Saturating arithmetic gives the same safe answer in both.\nLater enemies are tougher.\nThey should also pay better.\nNow, where does that reward live?\nIt belongs to the enemy.\nA slime knows how much health it has.\nIt should also know what it is worth:\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\n}\nAnd Enemy::for_stage() fills it in:\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\n}\n}\n}\nBecause every enemy is built through for_stage(), this is the only place that needed to change.\nNow run cargo test again.\ncargo run would build, with one more never-read warning, because the game only builds enemies through for_stage().\nThe tests do not build. attack_reduces_enemy_health builds an Enemy by hand, with a name and 10 health, and Rust wants a gold_reward there too.\nThat is the compiler finding the last place for us.\nInstead of making that test learn about rewards, let it start from a real enemy and adjust the one thing it cares about:\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nThe test still checks what it always checked: an attack takes health off the enemy.\nPaying out\nThe player has a wallet. The enemy has a price on its head.\nHanding one to the other is the player's job.\nAdd a method to impl Player, under new():\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\n&mut self is the same &mut self from click().\nThe method borrows the player, changes it, and gives it back.\nWhich brings us to defeat_enemy(), the one place in the game that already knows an enemy just died.\nIt grows:\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nThe order matters.\nWe pay the player before the enemy is replaced.\nOn the next line, self.enemy becomes a brand new slime with a brand new reward.\nIf we paid afterward, the player would get paid for an enemy they have not fought yet.\nQuestion: In defeat_enemy(), why does add_gold() come before self.enemy = Enemy::for_stage(self.stage)?\nRust requires method calls to come before assignments\nAfter the assignment, self.enemy.gold_reward belongs to the next enemy, not the one just defeated\nadd_gold() would not compile after the enemy is replaced\nAnswer: 2. After the assignment, self.enemy.gold_reward belongs to the next enemy, not the one just defeated\nExplanation: The code would still compile. It would just pay the wrong reward, which is why the order is a game rule and not something Rust can catch.\nRead defeat_enemy() out loud now:\nPay the player. Go to the next stage. Bring in the next enemy.\nNo arithmetic to decode.\nExperience and levels\nGold is something to spend.\nExperience, or XP, is something to keep.\nEvery defeated enemy should also give the player experience, and when they have enough, they level up.\nThat is two rules, so we need two numbers:\nhow much XP does an enemy give?\nhow much XP does the next level take?\nThe first one looks like the gold rule.\nPut it right under gold_reward_for_stage():\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nThe second one is new.\nEach level should take a little more than the last:\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nlevel 1 → 20 XP to reach level 2\nlevel 2 → 35 XP to reach level 3\nlevel 3 → 50 XP to reach level 4\nThat chain is split across lines so each step is easy to read. A line starting with . just continues the line above it.\nLevel 1 is the starting line. The saturating_sub(1) makes it count as zero steps taken, so level 1 needs the plain 20 and every level after that adds 15. Without it, the first level would already cost 35.\nThe exact numbers do not matter yet.\nThey have names and they will have tests.\nThe enemy carries its XP reward the same way it carries gold:\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nAdd it to Enemy::for_stage(), under gold_reward:\nxp_reward: xp_reward_for_stage(stage),\nAnd the player needs two more fields: how much XP they have, and what level they are:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nIn Player::new():\nxp: 0,\nlevel: 1,\nThat is now the normal place a fresh player is built, so future changes stay concentrated there.\nRun cargo test. Still eight, plus a few warnings that the new fields and xp_to_next_level() are never used. The next section uses them.\nLeveling up\nNow the interesting part.\nAdding XP is not like adding gold.\nGold just piles up.\nXP piles up until it crosses a line, and then something happens:\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nAdd it to impl Player, right under add_gold().\nWalk through it with a level 1 player who has 0 XP and just earned 25.\nThe XP becomes 25.\nLevel 1 needs 20. We have 25. That is enough, so we go into the loop.\nSpend 20 of it. XP is now 5. Level is now 2.\nLevel 2 needs 35. We have 5. The loop stops.\nThe player is level 2 with 5 XP toward the next one.\nWhy a while and not an if?\nBecause a reward might be big enough to cross two lines at once.\nGive that same level 1 player 60 XP.\nSpend 20 → level 2, 40 left.\nLevel 2 needs 35. 40 is enough. Spend 35 → level 3, 5 left.\nLevel 3 needs 50. The loop stops.\nAn if would have stopped at level 2 and left 40 XP sitting there, more than enough for another level the player never got.\nA while keeps going until the XP really is below the line.\nTry it: press Run and the 60 XP walkthrough plays out as level 3 with 5 XP left over. Then change while to if and run it again. The output becomes level 2 with 40 XP left over, which is the stuck player from above.\nLive example · swap the while for an if\nstruct Player {\nxp: u32,\nlevel: u32,\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nimpl Player {\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\n}\nfn main() {\nlet mut player = Player { xp: 0, level: 1 };\nplayer.add_xp(60);\nprintln!(\"level {} with {} XP left over\", player.level, player.xp);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/).\nQuestion: A level 1 player earns 60 XP at once. What does if instead of while leave behind?\nA player at level 3 with 5 XP\nA player at level 2 holding 40 XP, enough for another level they never got\nA compile error, because if cannot be used there\nAnswer: 2. A player at level 2 holding 40 XP, enough for another level they never got\nExplanation: if checks the line once. while keeps checking until the XP really is below it.\nYou might notice that xp_to_next_level(self.level) gets called twice in a row. That is fine. It is a tiny calculation, and for the same level it always gives the same answer.\nThat matters more than it looks right now. Later in this series, a player is going to come back after a few hours away and collect a very large pile of XP all at once.\nA plain minus sign\nLook at this line:\nself.xp -= xp_to_next_level(self.level);\n-= is shorthand: self.xp -= 20 means self.xp = self.xp - 20.\nThat is ordinary subtraction, not saturating_sub().\nEverywhere else in this program, subtraction goes through saturating_sub().\nHere, it is safe, and the line right above it is the reason.\nThe while only lets us in when self.xp is at least as big as the amount we are about to subtract.\nThe result can never drop below zero.\nsaturating_sub() is for when a number might go somewhere it should not.\nWhen the code right next to it proves that it cannot, plain subtraction says exactly what it means.\nQuestion: Why is the plain -= in add_xp() safe?\n-= quietly stops at zero, the same as saturating_sub()\nThe while condition only lets us in when self.xp is at least the amount being subtracted\nxp is a u32, and u32 can never go below zero\nAnswer: 2. The while condition only lets us in when self.xp is at least the amount being subtracted\nExplanation: A u32 cannot hold a negative number, so a plain subtraction that went below zero would be a bug, not a clamp. Here the loop condition rules that out.\nNow defeat_enemy() pays both:\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nWhat does a level actually do?\nRight now, nothing.\nThe player levels up, and hits exactly as hard as before.\nA level should make the player stronger.\nLook at Player again:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nbase_damage is a number we wrote down once, when the player was created, and never touched again.\nBut base damage should not be a fact about the player anymore.\nIt should be a consequence of the player's level.\nThat makes it a rule. Put it under xp_to_next_level():\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nlevel 1 → 5 damage\nlevel 2 → 6 damage\nlevel 3 → 7 damage\nLevel 1 gives 5, which is exactly the base_damage we have had since the beginning.\nNothing about the first fight changes.\nNow the field can go.\nRemove base_damage from the struct and from Player::new().\nRust will complain about one more place that still uses it. We will get to that in a second.\nIn its place, give Player a method that asks the rule, right under add_xp():\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\nThere is something new in that signature.\n&self, without the mut.\nSo far, our methods have needed &mut self because they changed something.\nA method that only needs to read takes &self instead. This is our first one.\nbase_damage() looks at the player's level and reports a number.\nIt does not change anything, so it does not ask for permission to.\nQuestion: Why does base_damage() take &self and add_xp() take &mut self?\n&self methods run faster\nadd_xp() changes the player, and base_damage() only reads it\nOnly the first method in an impl block may change the player\nAnswer: 2. add_xp() changes the player, and base_damage() only reads it\nExplanation: &mut self is the borrow that allows changes. A method that only reports a value asks for the smaller &self.\nOne thing still uses the old field: attack().\nIt reads player.base_damage, and that field no longer exists.\nRust says so (the line is wrapped here to fit):\nerror[E0615]: attempted to take value of method\n`base_damage` on type `&Player`\nand even suggests the fix: use parentheses to call the method.\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nThe parentheses are the only difference.\nplayer.base_damage was a number stored in the player.\nplayer.base_damage() is a number the player works out from its level.\nattack() did not have to learn anything about levels.\nIt asked for base damage. It got base damage.\nRun cargo test. All eight still pass, and that is the point: a level 1 player still deals 5 base damage, so nothing a test checks has changed.\nmain() shows the progress\nOur main() has been printing the enemy's health after every click.\nThat was useful when a fight took three clicks. Now there are rewards to watch, so let us print when something happens instead:\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage)\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\n}\n}\nBefore each click, main() remembers the stage and the level.\nAfter the click, it compares.\nStage went up? The enemy died, so say so, show the wallet, and introduce the next one.\nLevel went up? Say that too.\nNotice that the level check sits inside the stage check. It can, because XP is only ever handed out in defeat_enemy(). No enemy died, no XP, no level up.\nOne small thing about that first println!. {stage} works because stage is a plain variable. game.player.gold has dots in it, so it goes in the list after the string and a bare {} picks it up.\nmain() still does not know how leveling works.\nIt does borrow one rule, xp_to_next_level(), so it can show how far away the next level is. That is display, not a decision.\nIt clicks, it asks, and it prints what it was told.\nRun it:\ncargo run\nStage 1 cleared!  Gold: 5  XP: 6/20\nNext up: Training Slime with 30 health.\nStage 2 cleared!  Gold: 12  XP: 14/20\nNext up: Training Slime with 40 health.\nStage 3 cleared!  Gold: 21  XP: 4/35\nLevel up! Now level 2. Click damage: 8\nNext up: Training Slime with 50 health.\nStage 4 cleared!  Gold: 32  XP: 16/35\nNext up: Training Slime with 60 health.\nStage 5 cleared!  Gold: 45  XP: 30/35\nNext up: Training Slime with 70 health.\nStage 6 cleared!  Gold: 60  XP: 11/50\nLevel up! Now level 3. Click damage: 9\nNext up: Training Slime with 80 health.\nStage 7 cleared!  Gold: 77  XP: 29/50\nNext up: Training Slime with 90 health.\nStage 8 cleared!  Gold: 96  XP: 49/50\nNext up: Training Slime with 100 health.\nRead that top to bottom.\nGold climbs. XP climbs toward a line, crosses it, and resets with the leftover carried over. Click damage starts at 7 and climbs to 8, then 9.\nCompare that to where we started this article: 5 base damage plus 2 bonus, on stage 1 and on stage 30 alike.\nThe slimes still get tougher every stage. The difference is that now, so does the player.\nTesting the rules\nEach new rule gets a test for its numbers. Add these to the tests module:\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\nThen the behaviors.\nThe two walkthroughs from add_xp() become tests, so the explanation and the code can never drift apart:\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\nAnd the big one.\nThe last test in our starting point, defeating_an_enemy_advances_the_stage, checked that a win moved the game forward.\nNow a win should also pay.\nRename it and add two checks at the end:\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\nThis test does not care how damage is calculated, or how XP crosses a threshold.\nThose rules have their own tests now.\nIt asks the bigger question:\nWhen a click kills something, does the player get paid for it?\nRun them:\ncargo test\nAll thirteen should pass.\nThe complete program\nIf you are only reading along, skip to the last two sections.\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\n<details>\n<summary><code>src/main.rs</code> · complete 268-line program</summary>\nstruct Player {\nbonus_damage: u32,\ngold: u32,\nxp: u32,\nlevel: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\ngold_reward: u32,\nxp_reward: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Player {\nfn new() -> Player {\nPlayer {\nbonus_damage: 2,\ngold: 0,\nxp: 0,\nlevel: 1,\n}\n}\nfn add_gold(&mut self, amount: u32) {\nself.gold = self.gold.saturating_add(amount);\n}\nfn add_xp(&mut self, amount: u32) {\nself.xp = self.xp.saturating_add(amount);\nwhile self.xp >= xp_to_next_level(self.level) {\nself.xp -= xp_to_next_level(self.level);\nself.level = self.level.saturating_add(1);\n}\n}\nfn base_damage(&self) -> u32 {\nbase_damage_for_level(self.level)\n}\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\ngold_reward: gold_reward_for_stage(stage),\nxp_reward: xp_reward_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player::new(),\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.player.add_gold(self.enemy.gold_reward);\nself.player.add_xp(self.enemy.xp_reward);\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn gold_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(3)\n}\nfn xp_reward_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(2).saturating_add(4)\n}\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn base_damage_for_level(level: u32) -> u32 {\nlevel.saturating_add(4)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage(), player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 8 {\nlet stage = game.stage;\nlet level = game.player.level;\ngame.click();\nif game.stage > stage {\nprintln!(\n\"Stage {stage} cleared!  Gold: {}  XP: {}/{}\",\ngame.player.gold,\ngame.player.xp,\nxp_to_next_level(game.player.level)\n);\nif game.player.level > level {\nprintln!(\n\"Level up! Now level {}. Click damage: {}\",\ngame.player.level,\nclick_damage(game.player.base_damage(), game.player.bonus_damage)\n);\n}\nprintln!(\n\"  Next up: {} with {} health.\",\ngame.enemy.name, game.enemy.health\n);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player::new();\nlet mut enemy = Enemy::for_stage(1);\nenemy.health = 10;\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn later_stages_pay_better() {\nassert_eq!(gold_reward_for_stage(1), 5);\nassert_eq!(gold_reward_for_stage(2), 7);\nassert_eq!(xp_reward_for_stage(1), 6);\nassert_eq!(xp_reward_for_stage(2), 8);\n}\n#[test]\nfn levels_need_more_xp_as_they_go() {\nassert_eq!(xp_to_next_level(1), 20);\nassert_eq!(xp_to_next_level(2), 35);\n}\n#[test]\nfn leveling_up_keeps_leftover_xp() {\nlet mut player = Player::new();\nplayer.add_xp(25);\nassert_eq!(player.level, 2);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn a_big_reward_can_skip_levels() {\nlet mut player = Player::new();\nplayer.add_xp(60);\nassert_eq!(player.level, 3);\nassert_eq!(player.xp, 5);\n}\n#[test]\nfn levels_make_the_player_hit_harder() {\nassert_eq!(base_damage_for_level(1), 5);\nassert_eq!(base_damage_for_level(2), 6);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\nassert_eq!(game.player.gold, 0);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_pays_the_player() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\nassert_eq!(game.player.gold, 5);\nassert_eq!(game.player.xp, 6);\n}\n}\n</details>\nRun both:\ncargo run\ncargo test\nThe game should clear eight stages, and all thirteen tests should pass.\nWant to practice the while loop on its own? Here is a small one to finish.\nCoding challenge: total XP to reach a level\nWrite total_xp_to_reach(level): the XP a player has spent in total by the time they arrive at level. Level 1 is where everyone starts, so it costs nothing. Keep the name total_xp_to_reach and its level argument, because the checks call it. Press Check my code and it will tell you how you are doing.\nExpected result: it prints 0, 20, 55 and 105 for levels 1 to 4. Right now it prints 0 four times.\nCoding challenge · total XP to reach a level\nfn xp_to_next_level(level: u32) -> u32 {\nlevel\n.saturating_sub(1)\n.saturating_mul(15)\n.saturating_add(20)\n}\nfn total_xp_to_reach(level: u32) -> u32 {\n// TODO: add up xp_to_next_level for level 1, 2, ... up to (not including) `level`\n0\n}\nfn main() {\nfor level in 1..=4 {\nprintln!(\"level {level}: {} total XP\", total_xp_to_reach(level));\n}\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// Level 1 costs nothing, and arriving at level 2 costs what level 1 asks for. Does the loop stop before `level`?\n#[test]\nfn first_steps() {\nassert_eq!(total_xp_to_reach(1), 0);\nassert_eq!(total_xp_to_reach(2), 20);\n}\n/// Every level on the way has to be added, one at a time. The total grows with each step.\n#[test]\nfn adds_every_step() {\nassert_eq!(total_xp_to_reach(3), 55);\nassert_eq!(total_xp_to_reach(4), 105);\n}\n/// A huge level should not panic or wrap around. The hint names a method that stops at the ceiling.\n#[test]\nfn stops_at_the_ceiling() {\nassert_eq!(total_xp_to_reach(100_000), u32::MAX);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/).\nTip:\nKeep a running total (start it as let mut total: u32 = 0;) and a current level that starts at 1. While current is below level, add xp_to_next_level(current) to the total with saturating_add, then move current up by one.\nThe challenge is its own small program, so your src/main.rs stays exactly as it is above.\nWhat we learned\nAlong the way, we encountered:\nPlayer::new(), so a player is built in one place\n&self methods that read without changing\na while loop that keeps going until a condition stops being true\n-=, and plain subtraction where the code above it proves the number cannot go below zero\nrules that calculate a value instead of storing it\nThat is a short list, and the last item is the one worth keeping.\nbase_damage used to be a number sitting in the player.\nNow it is a rule applied to the player's level.\nThe player got smaller, and the game got bigger.\nHold onto that idea. We are going to do it again next time, to the other half of that damage calculation.\nGold with nothing to buy\nOur player finally gets stronger by winning.\nThere is just one problem.\nLook at the last line of that run again:\nStage 8 cleared!  Gold: 96  XP: 49/50\nNinety-six gold.\nWe call it money, but there is nothing in this game to spend it on.\ngold earned: 96\ngold spent:  0\nRight now it is not a reward.\nIt is a high score.\nNext time, we are going to give the player something to buy, and discover that the obvious way to report whether a purchase worked is not good enough.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-19T11:03:11Z",
      "id": "https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/",
      "image": "https://mnixblog.netlify.app/images/add-gold-xp-and-levels-in-rust/cover.jpg",
      "summary": "Learn Rust by making winning pay. Add gold, XP, and levels to an idle game, level up with a while loop, and calculate damage with a &self method.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Winning Is Not Progress.",
      "url": "https://mnixblog.netlify.app/add-gold-xp-and-levels-in-rust/"
    },
    {
      "content_html": "<p>Every fight in our game ends the same way.</p>\n<p>The slime dies.</p>\n<p>Then the program dies with it.</p>\n<p>So far, winning has been how our game ends.</p>\n<p>In an idle game, winning is how the game keeps going.</p>\n<p>One enemy falls, and the next one walks in.</p>\n<p>In this article, we are going to give our game a next stage, a tougher enemy waiting there, and one place to keep all of it.</p>\n<p>By the end, a whole turn of our game will fit in one line:</p>\n<pre><code class=\"language-rust\">game.click();\n</code></pre>\n<p>That one call will attack the enemy, notice when it dies, advance the stage, and send in the next one.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>So far, we have a player, a slime, and a way for one to hit the other.</p>\n<p>You need two structs and four small functions to follow along:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p><code>attack()</code> borrows the player to read it and the enemy to change it.</p>\n<p>Our combat loop looks like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        attack(&amp;player, &amp;mut enemy);\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n</code></pre>\n<p>Run it and the Training Slime goes down in three hits.</p>\n<p>Then the program is over.</p>\n<hr />\n<h2 id=\"the-next-enemy-needs-to-be-tougher\">The next enemy needs to be tougher<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#the-next-enemy-needs-to-be-tougher\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before we worry about where the next enemy comes from, we need to decide what it looks like.</p>\n<p>If every enemy had 20 health, stage 50 would feel exactly like stage 1.</p>\n<p>The next enemy should be tougher.</p>\n<p>That is a game rule, and we know what to do with game rules by now.</p>\n<p>Give it a small function:</p>\n<pre><code class=\"language-rust\">fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n</code></pre>\n<p>That gives us:</p>\n<pre><code class=\"language-text\">stage 1 → 20 health\nstage 2 → 30 health\nstage 3 → 40 health\n</code></pre>\n<p><code>saturating_mul()</code> is the multiplication version of the saturating arithmetic we already use.</p>\n<p>The exact balance does not matter yet.</p>\n<p>What matters is that the rule has a name and a test:</p>\n<pre><code class=\"language-rust\">#[test]\nfn later_stages_have_tougher_enemies() {\n    assert_eq!(enemy_health_for_stage(1), 20);\n    assert_eq!(enemy_health_for_stage(2), 30);\n}\n</code></pre>\n<p>Try it: change the numbers in the rule, or ask for a much later stage, and run it again.</p>\n<p>You should see 20, 30 and 40 health, then <code>Checks passed.</code></p>\n<p>The Playground only runs <code>#[test]</code> functions in its Test mode, so these checks use <code>assert_eq!</code> inside <code>main()</code> instead.</p>\n<p>If you change the formula, an <code>assert_eq!</code> fails and tells you which numbers disagreed.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the formula</figcaption><pre><code class=\"language-rust\">fn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn main() {\n    println!(&quot;stage 1 -&gt; {} health&quot;, enemy_health_for_stage(1));\n    println!(&quot;stage 2 -&gt; {} health&quot;, enemy_health_for_stage(2));\n    println!(&quot;stage 3 -&gt; {} health&quot;, enemy_health_for_stage(3));\n\n    assert_eq!(enemy_health_for_stage(1), 20);\n    assert_eq!(enemy_health_for_stage(2), 30);\n    println!(&quot;Checks passed.&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/\">the original article</a>.</p></figure>\n<p>If Rust warns that the function is never used yet, that is fine.</p>\n<p>We are about to use it.</p>\n<hr />\n<h2 id=\"try-it-in-main-first\">Try it in <code>main()</code> first<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#try-it-in-main-first\" aria-label=\"Link to this section\">#</a></h2>\n<p>The quickest way to make stages work is to put everything straight into <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut stage = 1;\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: enemy_health_for_stage(stage),\n    };\n\n    while stage &lt;= 3 {\n        attack(&amp;player, &amp;mut enemy);\n\n        if is_defeated(enemy.health) {\n            println!(\"Stage {stage} cleared!\");\n\n            stage = stage.saturating_add(1);\n\n            enemy = Enemy {\n                name: String::from(\"Training Slime\"),\n                health: enemy_health_for_stage(stage),\n            };\n        } else {\n            println!(\n                \"{} health: {}\",\n                enemy.name,\n                enemy.health\n            );\n        }\n    }\n}\n</code></pre>\n<p>We stop after stage 3 so the terminal does not scroll forever.</p>\n<p>Run it.</p>\n<p>It works.</p>\n<p>The slime falls, a tougher one takes its place, and the stages keep coming.</p>\n<p>It is also a mess.</p>\n<p>Look at what <code>main()</code> is doing now:</p>\n<pre><code class=\"language-text\">owns the player\nowns the enemy\nowns the stage\ndecides when a stage is cleared\nbuilds the next enemy\nprints everything\n</code></pre>\n<p>Only that last job really belongs in <code>main()</code>.</p>\n<p>Everything else is the game.</p>\n<p>Soon, defeating an enemy will also hand out rewards.</p>\n<p>If all of those rules stay here, <code>main()</code> becomes the game engine and the terminal UI at the same time.</p>\n<p>There is another reason to fix that now.</p>\n<p>Later, the browser will replace <code>main()</code>.</p>\n<p>The game rules need somewhere else to live.</p>\n<hr />\n<h2 id=\"give-the-game-a-shape\">Give the game a shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#give-the-game-a-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have solved this kind of problem before.</p>\n<p>Three loose numbers were not a player, so we built a <code>Player</code>.</p>\n<p>A stage, a player, and an enemy sitting loose in <code>main()</code> are not a game.</p>\n<p>So we build a <code>Game</code>:</p>\n<pre><code class=\"language-rust\">struct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n</code></pre>\n<p>This struct contains other structs:</p>\n<pre><code class=\"language-text\">Game\n├── stage: u32\n├── player: Player\n└── enemy: Enemy\n</code></pre>\n<p>A struct can hold types we made ourselves.</p>\n<p>The <code>Game</code> owns its <code>Player</code>.</p>\n<p>It owns its <code>Enemy</code>.</p>\n<p>There is now one value we can point at and say:</p>\n<blockquote>\n<p>This is the current state of the game.</p>\n</blockquote>\n<hr />\n<h2 id=\"where-does-a-new-enemy-come-from\">Where does a new enemy come from?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#where-does-a-new-enemy-come-from\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could create a game by spelling out every field manually:</p>\n<pre><code class=\"language-rust\">let mut game = Game {\n    stage: 1,\n    player: Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    },\n    enemy: Enemy {\n        name: String::from(\"Training Slime\"),\n        health: enemy_health_for_stage(1),\n    },\n};\n</code></pre>\n<p>That works.</p>\n<p>But we have already written this enemy more than once:</p>\n<pre><code class=\"language-rust\">Enemy {\n    name: String::from(\"Training Slime\"),\n    health: enemy_health_for_stage(stage),\n}\n</code></pre>\n<p>What we really want to say is:</p>\n<blockquote>\n<p>Give me the enemy for this stage.</p>\n</blockquote>\n<p>That sounds like a function.</p>\n<p>And because the function is specifically about creating an <code>Enemy</code>, it belongs with <code>Enemy</code>.</p>\n<hr />\n<h2 id=\"attach-functions-to-a-type-with-impl\">Attach functions to a type with <code>impl</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#attach-functions-to-a-type-with-impl\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust lets us attach functions to a type with an <code>impl</code> block:</p>\n<pre><code class=\"language-rust\">impl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n</code></pre>\n<p><code>impl Enemy</code> means:</p>\n<blockquote>\n<p>here are functions that belong to <code>Enemy</code></p>\n</blockquote>\n<p>Now we can write:</p>\n<pre><code class=\"language-rust\">Enemy::for_stage(1)\n</code></pre>\n<p>You have already seen this style of call:</p>\n<pre><code class=\"language-rust\">String::from(\"Training Slime\")\n</code></pre>\n<p>A function attached to a type that does not need an existing value is called an <strong>associated function</strong>.</p>\n<p><code>Enemy::for_stage()</code> does not replace our small health rule.</p>\n<p>It uses it:</p>\n<pre><code class=\"language-rust\">health: enemy_health_for_stage(stage),\n</code></pre>\n<p>One function decides how tough the enemy is.</p>\n<p>The other decides how to build one.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What separates <code>Enemy::for_stage(2)</code> from a method like <code>game.click()</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>for_stage</code> needs no existing value to run, while <code>click</code> works on a specific <code>Game</code> through <code>self</code></li>\n<li><code>for_stage</code> is a method, and <code>click</code> is an associated function</li>\n<li><code>click</code> can only be called from inside <code>main()</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>for_stage</code> needs no existing value to run, while <code>click</code> works on a specific <code>Game</code> through <code>self</code></p>\n<p>An associated function belongs to the type and is called with <code>::</code>. A method takes <code>self</code> and is called on a value with a dot.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"a-new-game\">A new game<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#a-new-game\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>Game</code> gets an <code>impl</code> block too:</p>\n<pre><code class=\"language-rust\">impl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n}\n</code></pre>\n<p>There is nothing magical about <code>new</code>.</p>\n<p>Rust has no special constructor keyword.</p>\n<p><code>new</code> is simply a common name for an associated function that creates a fresh value.</p>\n<p>Starting the game is now:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n</code></pre>\n<p>Game setup has one home.</p>\n<p>And it is no longer <code>main()</code>.</p>\n<hr />\n<h2 id=\"one-click\">One click<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#one-click\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now the part we have been building toward.</p>\n<p>What happens when the player clicks?</p>\n<p>Add this inside <code>impl Game</code>:</p>\n<pre><code class=\"language-rust\">fn click(&amp;mut self) {\n    attack(&amp;self.player, &amp;mut self.enemy);\n}\n</code></pre>\n<p>There is one new name in that signature:</p>\n<pre><code class=\"language-rust\">self\n</code></pre>\n<p><code>self</code> means:</p>\n<blockquote>\n<p>the value this method was called on</p>\n</blockquote>\n<p>When we write:</p>\n<pre><code class=\"language-rust\">game.click();\n</code></pre>\n<p>inside <code>click()</code>, <code>self</code> refers to <code>game</code>.</p>\n<p>That makes <code>click()</code> a <strong>method</strong>: a function attached to a type that works on a specific value of that type.</p>\n<p><code>Game::new()</code> does not take <code>self</code> because there is no game yet.</p>\n<p><code>click()</code> does because it works on a game that already exists.</p>\n<p>And <code>&amp;mut self</code> should look familiar.</p>\n<p>It means <code>click()</code> may temporarily change the game without taking ownership of it.</p>\n<p>That is why this needs:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n</code></pre>\n<p>Without <code>mut</code>, Rust refuses to let <code>click()</code> borrow the game mutably.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>let game = Game::new(); game.click();</code> fail to compile?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>Game::new()</code> returns a borrowed value</li>\n<li><code>click</code> takes <code>&amp;mut self</code>, and <code>game</code> was not declared <code>mut</code></li>\n<li><code>click</code> takes ownership of <code>game</code>, so it can only be called once</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>click</code> takes <code>&amp;mut self</code>, and <code>game</code> was not declared <code>mut</code></p>\n<p>The error is <code>E0596</code>: cannot borrow <code>game</code> as mutable, as it is not declared as mutable.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"two-borrows-one-game\">Two borrows, one game<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#two-borrows-one-game\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look inside <code>click()</code>:</p>\n<pre><code class=\"language-rust\">attack(&amp;self.player, &amp;mut self.enemy);\n</code></pre>\n<p>Last time, we learned:</p>\n<blockquote>\n<p><strong>For the same data at the same time: many readers, or one writer, but not both.</strong></p>\n</blockquote>\n<p>So why can we borrow part of the game to read and another part to change?</p>\n<p>Because they are not the same data.</p>\n<p><code>self.player</code> and <code>self.enemy</code> are different fields.</p>\n<p>They do not overlap.</p>\n<p>Rust can see that reading the player does not interfere with changing the enemy.</p>\n<p>Our <code>attack()</code> did not need to change.</p>\n<p>The borrowing simply moved inside the game.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why can <code>click()</code> write <code>attack(&amp;self.player, &amp;mut self.enemy)</code> when the rule says many readers or one writer, never both?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust does not apply borrowing rules to <code>self</code></li>\n<li>The reader borrows <code>self.player</code> and the writer borrows <code>self.enemy</code>, different fields that do not overlap</li>\n<li>Any <code>&amp;</code> and <code>&amp;mut</code> are allowed together when they appear in the same call</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The reader borrows <code>self.player</code> and the writer borrows <code>self.enemy</code>, different fields that do not overlap</p>\n<p>Borrowing <code>self.enemy</code> both ways at once, like <code>let p = &amp;self.enemy;</code> before the attack and using <code>p</code> after it, fails with <code>E0502</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"what-happens-when-the-enemy-dies\">What happens when the enemy dies?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#what-happens-when-the-enemy-dies\" aria-label=\"Link to this section\">#</a></h2>\n<p>Right now, <code>click()</code> only attacks.</p>\n<p>When the enemy reaches zero health, the next click would attack a corpse.</p>\n<p>That is grim.</p>\n<p>After every attack, <code>click()</code> should check whether the enemy was defeated:</p>\n<pre><code class=\"language-rust\">fn click(&amp;mut self) {\n    attack(&amp;self.player, &amp;mut self.enemy);\n\n    if is_defeated(self.enemy.health) {\n        self.defeat_enemy();\n    }\n}\n</code></pre>\n<p>Now we need <code>defeat_enemy()</code>:</p>\n<pre><code class=\"language-rust\">fn defeat_enemy(&amp;mut self) {\n    self.stage = self.stage.saturating_add(1);\n    self.enemy = Enemy::for_stage(self.stage);\n}\n</code></pre>\n<p>The stage goes up.</p>\n<p>The defeated enemy is replaced with the enemy for the new stage.</p>\n<p>We could put both lines directly inside <code>click()</code>.</p>\n<p>But defeating an enemy is its own event, and that event is going to grow.</p>\n<p>Soon it will also hand out rewards.</p>\n<p>Keeping it named means <code>click()</code> can stay readable:</p>\n<blockquote>\n<p>Attack the enemy. If it went down, handle the defeat.</p>\n</blockquote>\n<p>And <code>defeat_enemy()</code> says:</p>\n<blockquote>\n<p>Go to the next stage. Bring in the next enemy.</p>\n</blockquote>\n<p>Try it: here is the game so far, clicked three times.</p>\n<p>You should see <code>Stage 1: Training Slime health: 13</code>, then <code>Stage 1: Training Slime health: 6</code>, then <code>Stage 2: Training Slime health: 30</code>.</p>\n<p>Then try two experiments:</p>\n<ul>\n<li>delete the <code>if</code> block in <code>click()</code> and the third click leaves the slime at <code>health: 0</code> on stage 1, because nothing handles the defeat</li>\n<li>put the <code>if</code> block back and remove <code>mut</code> from <code>let mut game</code>, and Rust refuses with <code>E0596</code></li>\n</ul>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try removing the defeat check</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(&quot;Training Slime&quot;),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    game.click();\n    println!(&quot;Stage {}: {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n\n    game.click();\n    println!(&quot;Stage {}: {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n\n    game.click();\n    println!(&quot;Stage {}: {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/\">the original article</a>.</p></figure>\n<hr />\n<h2 id=\"main-gets-boring\"><code>main()</code> gets boring<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#main-gets-boring\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is the new <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\"Stage {stage} cleared!\");\n        } else {\n            println!(\n                \"{} health: {}\",\n                game.enemy.name,\n                game.enemy.health\n            );\n        }\n    }\n}\n</code></pre>\n<p><code>main()</code> still reads the game so it can decide what to print.</p>\n<p>But it does not:</p>\n<ul>\n<li>build enemies</li>\n<li>calculate their health</li>\n<li>decide when a stage advances</li>\n<li>handle a defeat</li>\n</ul>\n<p>It creates a game, clicks, and prints.</p>\n<p>Boring is exactly what we want.</p>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Training Slime health: 13\nTraining Slime health: 6\nStage 1 cleared!\nTraining Slime health: 23\nTraining Slime health: 16\nTraining Slime health: 9\nTraining Slime health: 2\nStage 2 cleared!\nTraining Slime health: 33\nTraining Slime health: 26\nTraining Slime health: 19\nTraining Slime health: 12\nTraining Slime health: 5\nStage 3 cleared!\n</code></pre>\n<p>Three stages.</p>\n<p>Each slime tougher than the last.</p>\n<p>And the game, not <code>main()</code>, decided all of that.</p>\n<hr />\n<h2 id=\"test-what-a-player-experiences\">Test what a player experiences<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#test-what-a-player-experiences\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our first tests check individual rules.</p>\n<p><code>Game</code> lets us start testing something larger: what happens when those rules work together.</p>\n<p>A new game should start at stage 1:</p>\n<pre><code class=\"language-rust\">#[test]\nfn a_new_game_starts_at_stage_one() {\n    let game = Game::new();\n\n    assert_eq!(game.stage, 1);\n    assert_eq!(game.enemy.health, 20);\n}\n</code></pre>\n<p>A click should hurt the enemy:</p>\n<pre><code class=\"language-rust\">#[test]\nfn clicking_damages_the_enemy() {\n    let mut game = Game::new();\n\n    game.click();\n\n    assert_eq!(game.enemy.health, 13);\n}\n</code></pre>\n<p>And defeating an enemy should move the game forward:</p>\n<pre><code class=\"language-rust\">#[test]\nfn defeating_an_enemy_advances_the_stage() {\n    let mut game = Game::new();\n    game.enemy.health = 1;\n\n    game.click();\n\n    assert_eq!(game.stage, 2);\n    assert_eq!(game.enemy.health, 30);\n}\n</code></pre>\n<p>That last test does not care how damage is calculated.</p>\n<p>Those smaller rules already have their own tests.</p>\n<p>It asks a larger question:</p>\n<blockquote>\n<p>When a click defeats an enemy, does the game move on?</p>\n</blockquote>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All eight should pass.</p>\n<p><strong>Coding challenge: Click many times</strong></p>\n<p>Add a method to <code>Game</code> called <code>click_many</code> that calls <code>self.click()</code> once for every click requested. Keep the name <code>click_many</code> and its <code>clicks</code> argument, because the checks call it. Press <strong>Check my code</strong> and it will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> the starter prints <code>Stage 1 - Training Slime health: 20</code>, and your finished version prints <code>Stage 2 - Training Slime health: 16</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Finish click_many</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(&quot;Training Slime&quot;),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    // TODO: call self.click() once for every click requested\n    fn click_many(&amp;mut self, clicks: u32) {}\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    game.click_many(5);\n\n    println!(&quot;Stage {} - {} health: {}&quot;, game.stage, game.enemy.name, game.enemy.health);\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// One click should be exactly one hit. Has click_many touched the enemy yet?\n#[test]\nfn one_click_is_one_hit() {\n    let mut game = Game::new();\n\n    game.click_many(1);\n\n    assert_eq!(game.enemy.health, 13);\n    assert_eq!(game.stage, 1);\n}\n\n/// Count your clicks. Ask for two and the slime should feel two hits, no more and no fewer.\n#[test]\nfn every_click_lands() {\n    let mut game = Game::new();\n\n    game.click_many(2);\n\n    assert_eq!(game.enemy.health, 6);\n}\n\n/// The slime should still fall mid-loop and the next one oozes in. Is each click going through self.click()?\n#[test]\nfn defeat_still_happens() {\n    let mut game = Game::new();\n\n    game.click_many(3);\n\n    assert_eq!(game.stage, 2);\n    assert_eq!(game.enemy.health, enemy_health_for_stage(2));\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Keep a counter that starts at 0, and use <code>while done &lt; clicks</code> to repeat. Add 1 to the counter with <code>saturating_add(1)</code> each time around.</p>\n</aside>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you are only reading along, you can skip to the last two sections.</p>\n<p>If you are coding along, here is everything in one place:</p>\n<details>\n<summary><code>src/main.rs</code> · complete 177-line program</summary>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nstruct Game {\n    stage: u32,\n    player: Player,\n    enemy: Enemy,\n}\n\nimpl Enemy {\n    fn for_stage(stage: u32) -&gt; Enemy {\n        Enemy {\n            name: String::from(\"Training Slime\"),\n            health: enemy_health_for_stage(stage),\n        }\n    }\n}\n\nimpl Game {\n    fn new() -&gt; Game {\n        Game {\n            stage: 1,\n            player: Player {\n                base_damage: 5,\n                bonus_damage: 2,\n            },\n            enemy: Enemy::for_stage(1),\n        }\n    }\n\n    fn click(&amp;mut self) {\n        attack(&amp;self.player, &amp;mut self.enemy);\n\n        if is_defeated(self.enemy.health) {\n            self.defeat_enemy();\n        }\n    }\n\n    fn defeat_enemy(&amp;mut self) {\n        self.stage = self.stage.saturating_add(1);\n        self.enemy = Enemy::for_stage(self.stage);\n    }\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn enemy_health_for_stage(stage: u32) -&gt; u32 {\n    stage.saturating_mul(10).saturating_add(10)\n}\n\nfn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let mut game = Game::new();\n\n    while game.stage &lt;= 3 {\n        let stage = game.stage;\n\n        game.click();\n\n        if game.stage &gt; stage {\n            println!(\"Stage {stage} cleared!\");\n        } else {\n            println!(\n                \"{} health: {}\",\n                game.enemy.name,\n                game.enemy.health\n            );\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player {\n            base_damage: 5,\n            bonus_damage: 2,\n        };\n\n        let mut enemy = Enemy {\n            name: String::from(\"Training Slime\"),\n            health: 10,\n        };\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n\n    #[test]\n    fn later_stages_have_tougher_enemies() {\n        assert_eq!(enemy_health_for_stage(1), 20);\n        assert_eq!(enemy_health_for_stage(2), 30);\n    }\n\n    #[test]\n    fn a_new_game_starts_at_stage_one() {\n        let game = Game::new();\n\n        assert_eq!(game.stage, 1);\n        assert_eq!(game.enemy.health, 20);\n    }\n\n    #[test]\n    fn clicking_damages_the_enemy() {\n        let mut game = Game::new();\n\n        game.click();\n\n        assert_eq!(game.enemy.health, 13);\n    }\n\n    #[test]\n    fn defeating_an_enemy_advances_the_stage() {\n        let mut game = Game::new();\n        game.enemy.health = 1;\n\n        game.click();\n\n        assert_eq!(game.stage, 2);\n        assert_eq!(game.enemy.health, 30);\n    }\n}\n</code></pre>\n</details>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should clear three stages, and all eight tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>structs that own other structs</li>\n<li><code>impl</code> blocks</li>\n<li>associated functions like <code>Enemy::for_stage()</code></li>\n<li><code>new()</code> as a convention, not a keyword</li>\n<li>methods</li>\n<li><code>self</code> and <code>&amp;mut self</code></li>\n<li>borrowing different fields at the same time</li>\n<li><code>saturating_mul()</code></li>\n<li>tests that describe larger game behavior</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is where the game lives.</p>\n<p>We started with a <code>main()</code> that owned everything and decided everything.</p>\n<p>We ended with:</p>\n<pre><code class=\"language-rust\">let mut game = Game::new();\n\ngame.click();\n</code></pre>\n<p>One click is now one full turn:</p>\n<pre><code class=\"language-text\">game.click()\n    ↓\nattack(&amp;self.player, &amp;mut self.enemy)\n    ↓\nclick_damage()\n    ↓\nhealth_after_hit()\n    ↓\nis_defeated()?\n    ↓\ndefeat_enemy()\n    ↓\nnext stage + Enemy::for_stage()\n</code></pre>\n<p>Look at the bottom of that chain.</p>\n<p><code>click_damage()</code>, <code>health_after_hit()</code>, and <code>is_defeated()</code> are the same small functions we started the series with.</p>\n<p>They have not changed.</p>\n<p>They are simply a few layers deeper now, inside something that finally looks like a game.</p>\n<hr />\n<h2 id=\"winning-does-not-pay-yet\">Winning does not pay yet<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#winning-does-not-pay-yet\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our game has a real loop now.</p>\n<p>Click. The slime falls. The next one walks in tougher than the last.</p>\n<p>But look at what the player gets for all that clicking:</p>\n<pre><code class=\"language-text\">gold:       nothing\nexperience: nothing\nlevels:     nothing\n</code></pre>\n<p>We just cleared three stages.</p>\n<p>Our player is exactly as strong as when the game started.</p>\n<p>Every slime gets tougher.</p>\n<p>Our player never does.</p>\n<p>Keep going long enough, and every new slime takes more clicks than the last one.</p>\n<p>Forever.</p>\n<p>That is not progression.</p>\n<p>That is a treadmill.</p>\n<p>An idle game needs a reason to keep clicking: rewards for winning, levels to reach, and something to spend those rewards on.</p>\n<p>And this time, we already know where those rewards will be handed out:</p>\n<pre><code class=\"language-rust\">fn defeat_enemy(&amp;mut self)\n</code></pre>\n<p>Next, we will make winning worth something.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/rust-game-state-impl-methods/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "One Fight Is Not a Game.\nCanonical: https://mnixblog.netlify.app/rust-game-state-impl-methods/\nPut Your Rust Game in One Place With impl and Methods\nEvery fight in our game ends the same way.\nThe slime dies.\nThen the program dies with it.\nSo far, winning has been how our game ends.\nIn an idle game, winning is how the game keeps going.\nOne enemy falls, and the next one walks in.\nIn this article, we are going to give our game a next stage, a tougher enemy waiting there, and one place to keep all of it.\nBy the end, a whole turn of our game will fit in one line:\ngame.click();\nThat one call will attack the enemy, notice when it dies, advance the stage, and send in the next one.\nOur starting point\nSo far, we have a player, a slime, and a way for one to hit the other.\nYou need two structs and four small functions to follow along:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nattack() borrows the player to read it and the enemy to change it.\nOur combat loop looks like this:\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nattack(&player, &mut enemy);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nRun it and the Training Slime goes down in three hits.\nThen the program is over.\nThe next enemy needs to be tougher\nBefore we worry about where the next enemy comes from, we need to decide what it looks like.\nIf every enemy had 20 health, stage 50 would feel exactly like stage 1.\nThe next enemy should be tougher.\nThat is a game rule, and we know what to do with game rules by now.\nGive it a small function:\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nThat gives us:\nstage 1 → 20 health\nstage 2 → 30 health\nstage 3 → 40 health\nsaturating_mul() is the multiplication version of the saturating arithmetic we already use.\nThe exact balance does not matter yet.\nWhat matters is that the rule has a name and a test:\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\nTry it: change the numbers in the rule, or ask for a much later stage, and run it again.\nYou should see 20, 30 and 40 health, then Checks passed.\nThe Playground only runs #[test] functions in its Test mode, so these checks use assert_eq! inside main() instead.\nIf you change the formula, an assert_eq! fails and tells you which numbers disagreed.\nLive example · try changing the formula\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn main() {\nprintln!(\"stage 1 -> {} health\", enemy_health_for_stage(1));\nprintln!(\"stage 2 -> {} health\", enemy_health_for_stage(2));\nprintln!(\"stage 3 -> {} health\", enemy_health_for_stage(3));\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\nprintln!(\"Checks passed.\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/).\nIf Rust warns that the function is never used yet, that is fine.\nWe are about to use it.\nTry it in main() first\nThe quickest way to make stages work is to put everything straight into main():\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut stage = 1;\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n};\nwhile stage <= 3 {\nattack(&player, &mut enemy);\nif is_defeated(enemy.health) {\nprintln!(\"Stage {stage} cleared!\");\nstage = stage.saturating_add(1);\nenemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n};\n} else {\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\n}\n}\nWe stop after stage 3 so the terminal does not scroll forever.\nRun it.\nIt works.\nThe slime falls, a tougher one takes its place, and the stages keep coming.\nIt is also a mess.\nLook at what main() is doing now:\nowns the player\nowns the enemy\nowns the stage\ndecides when a stage is cleared\nbuilds the next enemy\nprints everything\nOnly that last job really belongs in main().\nEverything else is the game.\nSoon, defeating an enemy will also hand out rewards.\nIf all of those rules stay here, main() becomes the game engine and the terminal UI at the same time.\nThere is another reason to fix that now.\nLater, the browser will replace main().\nThe game rules need somewhere else to live.\nGive the game a shape\nWe have solved this kind of problem before.\nThree loose numbers were not a player, so we built a Player.\nA stage, a player, and an enemy sitting loose in main() are not a game.\nSo we build a Game:\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nThis struct contains other structs:\nGame\n├── stage: u32\n├── player: Player\n└── enemy: Enemy\nA struct can hold types we made ourselves.\nThe Game owns its Player.\nIt owns its Enemy.\nThere is now one value we can point at and say:\nThis is the current state of the game.\nWhere does a new enemy come from?\nWe could create a game by spelling out every field manually:\nlet mut game = Game {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(1),\n},\n};\nThat works.\nBut we have already written this enemy more than once:\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\nWhat we really want to say is:\nGive me the enemy for this stage.\nThat sounds like a function.\nAnd because the function is specifically about creating an Enemy, it belongs with Enemy.\nAttach functions to a type with impl\nRust lets us attach functions to a type with an impl block:\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Enemy means:\nhere are functions that belong to Enemy\nNow we can write:\nEnemy::for_stage(1)\nYou have already seen this style of call:\nString::from(\"Training Slime\")\nA function attached to a type that does not need an existing value is called an associated function.\nEnemy::for_stage() does not replace our small health rule.\nIt uses it:\nhealth: enemy_health_for_stage(stage),\nOne function decides how tough the enemy is.\nThe other decides how to build one.\nQuestion: What separates Enemy::for_stage(2) from a method like game.click()?\nfor_stage needs no existing value to run, while click works on a specific Game through self\nfor_stage is a method, and click is an associated function\nclick can only be called from inside main()\nAnswer: 1. for_stage needs no existing value to run, while click works on a specific Game through self\nExplanation: An associated function belongs to the type and is called with ::. A method takes self and is called on a value with a dot.\nA new game\nGame gets an impl block too:\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\n}\nThere is nothing magical about new.\nRust has no special constructor keyword.\nnew is simply a common name for an associated function that creates a fresh value.\nStarting the game is now:\nlet mut game = Game::new();\nGame setup has one home.\nAnd it is no longer main().\nOne click\nNow the part we have been building toward.\nWhat happens when the player clicks?\nAdd this inside impl Game:\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\n}\nThere is one new name in that signature:\nself\nself means:\nthe value this method was called on\nWhen we write:\ngame.click();\ninside click(), self refers to game.\nThat makes click() a method: a function attached to a type that works on a specific value of that type.\nGame::new() does not take self because there is no game yet.\nclick() does because it works on a game that already exists.\nAnd &mut self should look familiar.\nIt means click() may temporarily change the game without taking ownership of it.\nThat is why this needs:\nlet mut game = Game::new();\nWithout mut, Rust refuses to let click() borrow the game mutably.\nQuestion: Why does let game = Game::new(); game.click(); fail to compile?\nGame::new() returns a borrowed value\nclick takes &mut self, and game was not declared mut\nclick takes ownership of game, so it can only be called once\nAnswer: 2. click takes &mut self, and game was not declared mut\nExplanation: The error is E0596: cannot borrow game as mutable, as it is not declared as mutable.\nTwo borrows, one game\nLook inside click():\nattack(&self.player, &mut self.enemy);\nLast time, we learned:\nFor the same data at the same time: many readers, or one writer, but not both.\nSo why can we borrow part of the game to read and another part to change?\nBecause they are not the same data.\nself.player and self.enemy are different fields.\nThey do not overlap.\nRust can see that reading the player does not interfere with changing the enemy.\nOur attack() did not need to change.\nThe borrowing simply moved inside the game.\nQuestion: Why can click() write attack(&self.player, &mut self.enemy) when the rule says many readers or one writer, never both?\nRust does not apply borrowing rules to self\nThe reader borrows self.player and the writer borrows self.enemy, different fields that do not overlap\nAny & and &mut are allowed together when they appear in the same call\nAnswer: 2. The reader borrows self.player and the writer borrows self.enemy, different fields that do not overlap\nExplanation: Borrowing self.enemy both ways at once, like let p = &self.enemy; before the attack and using p after it, fails with E0502.\nWhat happens when the enemy dies?\nRight now, click() only attacks.\nWhen the enemy reaches zero health, the next click would attack a corpse.\nThat is grim.\nAfter every attack, click() should check whether the enemy was defeated:\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nNow we need defeat_enemy():\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\nThe stage goes up.\nThe defeated enemy is replaced with the enemy for the new stage.\nWe could put both lines directly inside click().\nBut defeating an enemy is its own event, and that event is going to grow.\nSoon it will also hand out rewards.\nKeeping it named means click() can stay readable:\nAttack the enemy. If it went down, handle the defeat.\nAnd defeat_enemy() says:\nGo to the next stage. Bring in the next enemy.\nTry it: here is the game so far, clicked three times.\nYou should see Stage 1: Training Slime health: 13, then Stage 1: Training Slime health: 6, then Stage 2: Training Slime health: 30.\nThen try two experiments:\ndelete the if block in click() and the third click leaves the slime at health: 0 on stage 1, because nothing handles the defeat\nput the if block back and remove mut from let mut game, and Rust refuses with E0596\nLive example · try removing the defeat check\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\ngame.click();\nprintln!(\"Stage {}: {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\ngame.click();\nprintln!(\"Stage {}: {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\ngame.click();\nprintln!(\"Stage {}: {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/).\nmain() gets boring\nHere is the new main():\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\ngame.click();\nif game.stage > stage {\nprintln!(\"Stage {stage} cleared!\");\n} else {\nprintln!(\n\"{} health: {}\",\ngame.enemy.name,\ngame.enemy.health\n);\n}\n}\n}\nmain() still reads the game so it can decide what to print.\nBut it does not:\nbuild enemies\ncalculate their health\ndecide when a stage advances\nhandle a defeat\nIt creates a game, clicks, and prints.\nBoring is exactly what we want.\nRun:\ncargo run\nYou should see:\nTraining Slime health: 13\nTraining Slime health: 6\nStage 1 cleared!\nTraining Slime health: 23\nTraining Slime health: 16\nTraining Slime health: 9\nTraining Slime health: 2\nStage 2 cleared!\nTraining Slime health: 33\nTraining Slime health: 26\nTraining Slime health: 19\nTraining Slime health: 12\nTraining Slime health: 5\nStage 3 cleared!\nThree stages.\nEach slime tougher than the last.\nAnd the game, not main(), decided all of that.\nTest what a player experiences\nOur first tests check individual rules.\nGame lets us start testing something larger: what happens when those rules work together.\nA new game should start at stage 1:\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\nA click should hurt the enemy:\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\nAnd defeating an enemy should move the game forward:\n#[test]\nfn defeating_an_enemy_advances_the_stage() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\n}\nThat last test does not care how damage is calculated.\nThose smaller rules already have their own tests.\nIt asks a larger question:\nWhen a click defeats an enemy, does the game move on?\nRun:\ncargo test\nAll eight should pass.\nCoding challenge: Click many times\nAdd a method to Game called click_many that calls self.click() once for every click requested. Keep the name click_many and its clicks argument, because the checks call it. Press Check my code and it will tell you how you are doing.\nExpected result: the starter prints Stage 1 - Training Slime health: 20, and your finished version prints Stage 2 - Training Slime health: 16.\nFinish click_many\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\n// TODO: call self.click() once for every click requested\nfn click_many(&mut self, clicks: u32) {}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\ngame.click_many(5);\nprintln!(\"Stage {} - {} health: {}\", game.stage, game.enemy.name, game.enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// One click should be exactly one hit. Has click_many touched the enemy yet?\n#[test]\nfn one_click_is_one_hit() {\nlet mut game = Game::new();\ngame.click_many(1);\nassert_eq!(game.enemy.health, 13);\nassert_eq!(game.stage, 1);\n}\n/// Count your clicks. Ask for two and the slime should feel two hits, no more and no fewer.\n#[test]\nfn every_click_lands() {\nlet mut game = Game::new();\ngame.click_many(2);\nassert_eq!(game.enemy.health, 6);\n}\n/// The slime should still fall mid-loop and the next one oozes in. Is each click going through self.click()?\n#[test]\nfn defeat_still_happens() {\nlet mut game = Game::new();\ngame.click_many(3);\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, enemy_health_for_stage(2));\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/).\nTip:\nKeep a counter that starts at 0, and use while done < clicks to repeat. Add 1 to the counter with saturating_add(1) each time around.\nThe complete program\nIf you are only reading along, you can skip to the last two sections.\nIf you are coding along, here is everything in one place:\n<details>\n<summary><code>src/main.rs</code> · complete 177-line program</summary>\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nstruct Game {\nstage: u32,\nplayer: Player,\nenemy: Enemy,\n}\nimpl Enemy {\nfn for_stage(stage: u32) -> Enemy {\nEnemy {\nname: String::from(\"Training Slime\"),\nhealth: enemy_health_for_stage(stage),\n}\n}\n}\nimpl Game {\nfn new() -> Game {\nGame {\nstage: 1,\nplayer: Player {\nbase_damage: 5,\nbonus_damage: 2,\n},\nenemy: Enemy::for_stage(1),\n}\n}\nfn click(&mut self) {\nattack(&self.player, &mut self.enemy);\nif is_defeated(self.enemy.health) {\nself.defeat_enemy();\n}\n}\nfn defeat_enemy(&mut self) {\nself.stage = self.stage.saturating_add(1);\nself.enemy = Enemy::for_stage(self.stage);\n}\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn enemy_health_for_stage(stage: u32) -> u32 {\nstage.saturating_mul(10).saturating_add(10)\n}\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nfn main() {\nlet mut game = Game::new();\nwhile game.stage <= 3 {\nlet stage = game.stage;\ngame.click();\nif game.stage > stage {\nprintln!(\"Stage {stage} cleared!\");\n} else {\nprintln!(\n\"{} health: {}\",\ngame.enemy.name,\ngame.enemy.health\n);\n}\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n#[test]\nfn later_stages_have_tougher_enemies() {\nassert_eq!(enemy_health_for_stage(1), 20);\nassert_eq!(enemy_health_for_stage(2), 30);\n}\n#[test]\nfn a_new_game_starts_at_stage_one() {\nlet game = Game::new();\nassert_eq!(game.stage, 1);\nassert_eq!(game.enemy.health, 20);\n}\n#[test]\nfn clicking_damages_the_enemy() {\nlet mut game = Game::new();\ngame.click();\nassert_eq!(game.enemy.health, 13);\n}\n#[test]\nfn defeating_an_enemy_advances_the_stage() {\nlet mut game = Game::new();\ngame.enemy.health = 1;\ngame.click();\nassert_eq!(game.stage, 2);\nassert_eq!(game.enemy.health, 30);\n}\n}\n</details>\nRun both:\ncargo run\ncargo test\nThe game should clear three stages, and all eight tests should pass.\nWhat we learned\nAlong the way, we encountered:\nstructs that own other structs\nimpl blocks\nassociated functions like Enemy::for_stage()\nnew() as a convention, not a keyword\nmethods\nself and &mut self\nborrowing different fields at the same time\nsaturating_mul()\ntests that describe larger game behavior\nYou do not need to memorize all of that yet.\nThe important change is where the game lives.\nWe started with a main() that owned everything and decided everything.\nWe ended with:\nlet mut game = Game::new();\ngame.click();\nOne click is now one full turn:\ngame.click()\n↓\nattack(&self.player, &mut self.enemy)\n↓\nclick_damage()\n↓\nhealth_after_hit()\n↓\nis_defeated()?\n↓\ndefeat_enemy()\n↓\nnext stage + Enemy::for_stage()\nLook at the bottom of that chain.\nclick_damage(), health_after_hit(), and is_defeated() are the same small functions we started the series with.\nThey have not changed.\nThey are simply a few layers deeper now, inside something that finally looks like a game.\nWinning does not pay yet\nOur game has a real loop now.\nClick. The slime falls. The next one walks in tougher than the last.\nBut look at what the player gets for all that clicking:\ngold:       nothing\nexperience: nothing\nlevels:     nothing\nWe just cleared three stages.\nOur player is exactly as strong as when the game started.\nEvery slime gets tougher.\nOur player never does.\nKeep going long enough, and every new slime takes more clicks than the last one.\nForever.\nThat is not progression.\nThat is a treadmill.\nAn idle game needs a reason to keep clicking: rewards for winning, levels to reach, and something to spend those rewards on.\nAnd this time, we already know where those rewards will be handed out:\nfn defeat_enemy(&mut self)\nNext, we will make winning worth something.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/rust-game-state-impl-methods/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-17T19:42:53Z",
      "id": "https://mnixblog.netlify.app/rust-game-state-impl-methods/",
      "image": "https://mnixblog.netlify.app/images/rust-game-state-impl-methods/cover.png",
      "summary": "Learn Rust impl blocks and methods by giving your idle game one Game struct, tougher enemies by stage, and a game.click() that runs a full turn.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "One Fight Is Not a Game.",
      "url": "https://mnixblog.netlify.app/rust-game-state-impl-methods/"
    },
    {
      "content_html": "<p>Our game has a <code>Player</code> and an <code>Enemy</code></p>\n<p>Then, the moment they fight, we strip them back down for parts:</p>\n<pre><code class=\"language-rust\">enemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>We built two real game types.</p>\n<p>The moment they interact, we go back to passing loose numbers around.</p>\n<p>In this article, we are going to change <code>attack()</code> so it takes the player and the enemy themselves.</p>\n<p>Along the way, Rust will refuse to compile our code three times.</p>\n<p>Every time, it will be right.</p>\n<p>By the end, our combat loop will say what the game actually does:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n</code></pre>\n<p>A player attacks an enemy.</p>\n<p>Nothing gets torn apart.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>You need two structs and four small functions to follow along:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>At the bottom of <code>main.rs</code>, the <code>tests</code> module holds three tests for the small rules and one test that calls <code>attack()</code> directly:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    assert_eq!(attack(10, 5, 2), 3);\n}\n</code></pre>\n<p>We will come back to that test once <code>attack()</code> changes.</p>\n<p>Our combat loop looks like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(\n            enemy.health,\n            player.base_damage,\n            player.bonus_damage,\n        );\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n</code></pre>\n<p>It works.</p>\n<p>But <code>attack()</code> does not know what a player is.</p>\n<p>It does not know what an enemy is.</p>\n<p>It takes three numbers and hands back a fourth, and <code>main()</code> has to put that number back where it belongs.</p>\n<p>What we want to write is closer to:</p>\n<pre><code class=\"language-rust\">attack(player, enemy);\n</code></pre>\n<p>So let us try that.</p>\n<hr />\n<h2 id=\"hand-the-player-to-attack\">Hand the player to <code>attack()</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#hand-the-player-to-attack\" aria-label=\"Link to this section\">#</a></h2>\n<p>Let us start with the player and leave the enemy alone for a moment.</p>\n<p>Change <code>attack()</code> so it takes a whole <code>Player</code> instead of two loose damage values:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: Player,\n    health: u32,\n) -&gt; u32 {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>The body barely changed.</p>\n<p>We read the damage values from the player with the same dot syntax we used in <code>main()</code>.</p>\n<p>Now update the call inside the loop:</p>\n<pre><code class=\"language-rust\">enemy.health =\n    attack(player, enemy.health);\n</code></pre>\n<p>That reads much better.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>Rust refuses to build the program.</p>\n<p>Trimmed down, the error says:</p>\n<pre><code class=\"language-text\">error[E0382]: use of moved value: `player`\n\nvalue moved here, in previous\niteration of loop\n</code></pre>\n<p>The full message is longer, and it suggests a few fixes, like cloning the player.</p>\n<p>Some of them would work.</p>\n<p>None of them is what we want, so let us figure out what actually happened.</p>\n<p>The first attack would have been fine.</p>\n<p>The second attack is the problem.</p>\n<p>Where did the player go?</p>\n<hr />\n<h2 id=\"the-player-moved\">The player moved<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#the-player-moved\" aria-label=\"Link to this section\">#</a></h2>\n<p>In Rust, every value has an owner.</p>\n<p>Right now, <code>main()</code> owns <code>player</code>.</p>\n<p>It created it, so it is responsible for it.</p>\n<p>When we write:</p>\n<pre><code class=\"language-rust\">attack(player, enemy.health)\n</code></pre>\n<p>and <code>attack()</code> asks for a <code>Player</code>, we are not lending the player to <code>attack()</code>.</p>\n<p>We are giving it away.</p>\n<p>Ownership moves into <code>attack()</code>.</p>\n<p>When <code>attack()</code> finishes, the <code>Player</code> it owns is no longer available back in <code>main()</code>.</p>\n<p>So on the next trip around the loop, <code>main()</code> tries to hand over a player it no longer owns.</p>\n<p>Rust catches that before the program ever runs.</p>\n<p>In game terms, our player attacked once and then vanished.</p>\n<p>That is a bold balancing decision.</p>\n<p>It is not ours.</p>\n<p>The attack was supposed to defeat the enemy.</p>\n<p>Not the player.</p>\n<hr />\n<h2 id=\"why-did-health-never-move\">Why did health never move?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#why-did-health-never-move\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have been passing <code>enemy.health</code> into functions all along, and it never disappeared.</p>\n<p>That is because <code>u32</code> implements Rust's <code>Copy</code> trait.</p>\n<p>When we pass a <code>u32</code> by value, Rust can automatically make a copy, so the original remains available.</p>\n<p>Our <code>Player</code> does not implement <code>Copy</code>, so passing it by value moves ownership instead.</p>\n<p>You do not need to understand traits yet.</p>\n<p>For now, the useful difference is:</p>\n<pre><code class=\"language-text\">u32     → copied\nPlayer  → moved\n</code></pre>\n<p>A <code>u32</code> gets photocopied.</p>\n<p>A <code>Player</code> gets handed over.</p>\n<p>We do not want to give the player away.</p>\n<p>We want <code>attack()</code> to look at it.</p>\n<p>Try it: this is the program with <code>attack()</code> taking a whole <code>Player</code>, and it fails to compile on purpose.</p>\n<p>Run it and read the error for yourself.</p>\n<p>You should see error code <code>E0382</code>, \"use of moved value\", pointing at the call inside the loop.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · read the move error</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(player: Player, health: u32) -&gt; u32 {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(player, enemy.health);\n\n        println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n    }\n\n    println!(&quot;{} defeated!&quot;, enemy.name);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/\">the original article</a>.</p></figure>\n<p>The next section fixes it with one character in two places.</p>\n<p>If you want to fix it first, ask yourself what <code>attack()</code> needs to do with the player.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After <code>attack(player, enemy.health)</code> runs once, why can <code>main()</code> still use <code>enemy.health</code> but not <code>player</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Fields of a struct are never moved</li>\n<li><code>u32</code> implements <code>Copy</code>, so a copy was passed, but <code>Player</code> does not, so it moved</li>\n<li>Rust only tracks moves for values that contain a <code>String</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>u32</code> implements <code>Copy</code>, so a copy was passed, but <code>Player</code> does not, so it moved</p>\n<p>Passing a <code>u32</code> by value copies it. <code>Player</code> is not <code>Copy</code>, so passing it by value hands ownership to <code>attack()</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"borrow-the-player-with\">Borrow the player with <code>&amp;</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#borrow-the-player-with\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust lets a function borrow a value instead of owning it.</p>\n<p>Add one character to the parameter type:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: &amp;Player,\n    health: u32,\n) -&gt; u32 {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p><code>&amp;Player</code> means:</p>\n<blockquote>\n<p>a reference to a <code>Player</code> that someone else owns</p>\n</blockquote>\n<p>A reference lets <code>attack()</code> reach the player without taking ownership of it.</p>\n<p>When the call finishes, the player is still sitting in <code>main()</code> where we left it.</p>\n<p>Notice that the body did not change.</p>\n<p>The dot works the same way through a reference, so:</p>\n<pre><code class=\"language-rust\">player.base_damage\n</code></pre>\n<p>still reads the field.</p>\n<p>The call needs the same <code>&amp;</code>:</p>\n<pre><code class=\"language-rust\">enemy.health =\n    attack(&amp;player, enemy.health);\n</code></pre>\n<p><code>&amp;player</code> says:</p>\n<blockquote>\n<p>let <code>attack()</code> borrow the player for this call</p>\n</blockquote>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>The Training Slime goes down in three hits again.</p>\n<p>The player survives every one of them.</p>\n<p>There is a bonus hiding in that <code>&amp;</code>.</p>\n<p>A plain <code>&amp;</code> reference gives <code>attack()</code> read-only access to the player's fields.</p>\n<p>If <code>attack()</code> tried to assign a new value to:</p>\n<pre><code class=\"language-rust\">player.base_damage\n</code></pre>\n<p>Rust would refuse to compile it.</p>\n<p>That is exactly the promise we want.</p>\n<p>An attack needs to read the player.</p>\n<p>It does not need to change the player.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Inside <code>fn attack(player: &amp;Player, ...)</code>, what does the line <code>player.base_damage = 9;</code> do?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Changes the caller's player, since a reference points at it</li>\n<li>Fails to compile, because a plain <code>&amp;</code> reference is read-only</li>\n<li>Compiles, but only changes a private copy</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Fails to compile, because a plain <code>&amp;</code> reference is read-only</p>\n<p>Rust rejects it with <code>E0594</code>. The reference lets <code>attack()</code> read the player, not write to it.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"now-the-enemy\">Now the enemy<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#now-the-enemy\" aria-label=\"Link to this section\">#</a></h2>\n<p>The player is solved.</p>\n<p>The enemy is still split apart:</p>\n<pre><code class=\"language-rust\">enemy.health =\n    attack(&amp;player, enemy.health);\n</code></pre>\n<p><code>attack()</code> gets a copy of the enemy's health, calculates a new number, and returns it.</p>\n<p>Then <code>main()</code> writes that number back into the enemy.</p>\n<p>What we really want is for <code>attack()</code> to change the enemy itself.</p>\n<p>We just learned that borrowing keeps a value where it is.</p>\n<p>Let us borrow the enemy too:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: &amp;Player,\n    enemy: &amp;Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p>Two things changed besides the enemy parameter.</p>\n<p>There is no:</p>\n<pre><code class=\"language-rust\">-&gt; u32\n</code></pre>\n<p>anymore.</p>\n<p><code>attack()</code> is not handing back a number for someone else to store.</p>\n<p>The result lives in the enemy.</p>\n<p>Inside the function, we assign the new health directly to:</p>\n<pre><code class=\"language-rust\">enemy.health\n</code></pre>\n<p>Update the call in <code>main()</code>:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;enemy);\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>Rust refuses again:</p>\n<pre><code class=\"language-text\">error[E0594]: cannot assign to\n`enemy.health`, which is behind\na `&amp;` reference\n</code></pre>\n<p>You may also see a warning that <code>enemy</code> does not need to be mutable.</p>\n<p>Ignore it for a moment.</p>\n<p>We are about to need that <code>mut</code> again.</p>\n<p>This is the promise from the last section working against us.</p>\n<p><code>&amp;Enemy</code> lets us look at the enemy.</p>\n<p>We did not come here to look.</p>\n<p>A plain <code>&amp;</code> reference is read-only.</p>\n<p>That was perfect for the player.</p>\n<p>It is wrong for the enemy because the entire point of an attack is that the enemy's health changes.</p>\n<hr />\n<h2 id=\"borrow-the-enemy-with-mut\">Borrow the enemy with <code>&amp;mut</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#borrow-the-enemy-with-mut\" aria-label=\"Link to this section\">#</a></h2>\n<p>We have already met <code>mut</code>.</p>\n<p>It is how we told Rust that our enemy is allowed to change.</p>\n<p>References can express the same idea.</p>\n<p>Change:</p>\n<pre><code class=\"language-rust\">&amp;Enemy\n</code></pre>\n<p>to:</p>\n<pre><code class=\"language-rust\">&amp;mut Enemy\n</code></pre>\n<p>Our attack becomes:</p>\n<pre><code class=\"language-rust\">fn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n</code></pre>\n<p><code>&amp;mut Enemy</code> means:</p>\n<blockquote>\n<p>a mutable reference to an <code>Enemy</code> that someone else owns</p>\n</blockquote>\n<p>In game terms:</p>\n<blockquote>\n<p><code>attack()</code> may temporarily change this enemy, but it does not own the enemy.</p>\n</blockquote>\n<p>The call needs to ask for the same thing:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n</code></pre>\n<p>This only works because <code>enemy</code> was already declared with:</p>\n<pre><code class=\"language-rust\">let mut enemy\n</code></pre>\n<p>Rust will not let us borrow something mutably if the owner never agreed that it could change.</p>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Training Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\n</code></pre>\n<p>The same fight as before.</p>\n<p>But now the signature tells the whole story:</p>\n<pre><code class=\"language-text\">&amp;Player     → attack reads the player\n&amp;mut Enemy  → attack changes the enemy\n</code></pre>\n<p>Nobody gets consumed.</p>\n<p>Nobody gets torn apart.</p>\n<p>Both are still in <code>main()</code>, ready for the next attack.</p>\n<p>Try it: here is a small version of the fixed program that attacks twice.</p>\n<p>Run it first.</p>\n<p>You should see <code>Training Slime health: 13</code>, then <code>Training Slime health: 6</code>.</p>\n<p>Then break it on purpose, one change at a time, and read each error:</p>\n<ul>\n<li>change <code>enemy: &amp;mut Enemy</code> to <code>enemy: &amp;Enemy</code> in <code>attack()</code> and you get <code>E0594</code>, the same error as before</li>\n<li>put <code>enemy: &amp;mut Enemy</code> back, then remove <code>mut</code> from <code>let mut enemy</code> and you get <code>E0596</code></li>\n</ul>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try breaking the borrow</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    attack(&amp;player, &amp;mut enemy);\n    println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n\n    attack(&amp;player, &amp;mut enemy);\n    println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>attack(&amp;player, &amp;mut enemy)</code> need <code>let mut enemy</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Because <code>&amp;mut</code> makes Rust copy the enemy first</li>\n<li>Because Rust will not lend out a mutable borrow of a value its owner never declared changeable</li>\n<li>It does not; <code>&amp;mut enemy</code> works on any variable</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Because Rust will not lend out a mutable borrow of a value its owner never declared changeable</p>\n<p>Without <code>mut</code> on the variable, the borrow fails with <code>E0596</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"one-rule-worth-knowing-now\">One rule worth knowing now<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#one-rule-worth-knowing-now\" aria-label=\"Link to this section\">#</a></h2>\n<p>There is one borrowing rule you will run into early, so here it is in game terms.</p>\n<p>Suppose that, instead of the combat loop, <code>main()</code> grabs a reference to the enemy's name before attacking:</p>\n<pre><code class=\"language-rust\">let name = &amp;enemy.name;\n\nattack(&amp;player, &amp;mut enemy);\n\nprintln!(\"{name} took a hit!\");\n</code></pre>\n<p>Rust refuses for the third time:</p>\n<pre><code class=\"language-text\">error[E0502]: cannot borrow `enemy`\nas mutable because it is also\nborrowed as immutable\n</code></pre>\n<p>A borrow remains active until its last use.</p>\n<p><code>name</code> is used by the <code>println!()</code> after the attack, so that borrow is still active when <code>attack()</code> asks for permission to change the enemy.</p>\n<p>Rust will not hand out a mutable borrow while that reading borrow is still being used.</p>\n<p>Our combat loop never hits this problem because each borrow is finished before the next conflicting access begins:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n\nprintln!(\n    \"{} health: {}\",\n    enemy.name,\n    enemy.health\n);\n</code></pre>\n<p>In this case, the mutable borrow is finished when <code>attack()</code> returns.</p>\n<p>Then <code>println!()</code> is free to read the enemy.</p>\n<p>That is the beginner version of the rule:</p>\n<blockquote>\n<p><strong>For the same data at the same time: many readers, or one writer, but not both.</strong></p>\n</blockquote>\n<p>You do not need more than that today.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does Rust reject reading <code>name = &amp;enemy.name</code> before <code>attack(&amp;player, &amp;mut enemy)</code> when <code>name</code> is printed afterward?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>A <code>&amp;</code> borrow can never be followed by a <code>&amp;mut</code> borrow of the same value</li>\n<li>The <code>&amp;</code> borrow is still in use when <code>attack()</code> asks for <code>&amp;mut</code>, so a reader and a writer overlap</li>\n<li><code>attack()</code> takes ownership of <code>enemy</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The <code>&amp;</code> borrow is still in use when <code>attack()</code> asks for <code>&amp;mut</code>, so a reader and a writer overlap</p>\n<p>A borrow lasts until its last use. If nothing used <code>name</code> after the attack, the program would compile.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: End the borrow in time</strong></p>\n<p>This starter fails to compile on purpose, because the lesson here is the error.</p>\n<p>Fix it without changing <code>attack()</code>. Keep the names <code>attack</code> and <code>main</code> and the shape of <code>Player</code> and <code>Enemy</code>, because the checks call them. Press <strong>Check my code</strong> and it will tell you how you are doing.</p>\n<p><strong>Expected result:</strong> it compiles and prints <code>Training Slime took a hit! Health: 13</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Fix the borrow conflict</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(player: &amp;Player, enemy: &amp;mut Enemy) {\n    let damage = click_damage(player.base_damage, player.bonus_damage);\n\n    enemy.health = health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    // TODO: this borrow is still in use after the attack. Make the program compile.\n    let name = &amp;enemy.name;\n\n    attack(&amp;player, &amp;mut enemy);\n\n    println!(&quot;{name} took a hit! Health: {}&quot;, enemy.health);\n}\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// It compiles now, and the program still runs from start to finish.\n#[test]\nfn main_still_runs() {\n    main();\n}\n\n/// It compiles, but attack() no longer does its job. We only wanted to move the borrow, not the damage.\n#[test]\nfn attack_still_lands() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    attack(&amp;player, &amp;mut enemy);\n\n    assert_eq!(enemy.health, 13);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>The error is <code>E0502</code>. A borrow ends at its last use, so ask where <code>name</code> needs to start borrowing.</p>\n</aside>\n<hr />\n<h2 id=\"update-the-test\">Update the test<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#update-the-test\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our <code>attack()</code> test still passes three loose numbers, so it needs the new signature:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 10,\n    };\n\n    attack(&amp;player, &amp;mut enemy);\n\n    assert_eq!(enemy.health, 3);\n}\n</code></pre>\n<p>The rule it checks has not changed.</p>\n<p>An attack with five base damage and two bonus damage takes ten health down to three.</p>\n<p>What changed is how the test says it.</p>\n<p>It builds a player.</p>\n<p>It builds an enemy.</p>\n<p>It lets one attack the other.</p>\n<p>Then it checks the enemy afterward.</p>\n<p>That reads like the game.</p>\n<p>The other three tests do not touch <code>attack()</code>, so they stay exactly as they were.</p>\n<p>Run them:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All four should pass.</p>\n<p>We do not need a separate test proving that <code>attack()</code> does not change the player.</p>\n<p>With our current <code>Player</code> type, an <code>&amp;Player</code> reference does not allow <code>attack()</code> to assign new values to the player's fields.</p>\n<p>The compiler checks that for us every time we build.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>If you have been following along in your editor, here is everything in one place.</p>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    player: &amp;Player,\n    enemy: &amp;mut Enemy,\n) {\n    let damage = click_damage(\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    enemy.health =\n        health_after_hit(enemy.health, damage);\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        attack(&amp;player, &amp;mut enemy);\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        let player = Player {\n            base_damage: 5,\n            bonus_damage: 2,\n        };\n\n        let mut enemy = Enemy {\n            name: String::from(\"Training Slime\"),\n            health: 10,\n        };\n\n        attack(&amp;player, &amp;mut enemy);\n\n        assert_eq!(enemy.health, 3);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The Training Slime should fall in three hits, and all four tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>\n<p>ownership</p>\n</li>\n<li>\n<p>moves</p>\n</li>\n<li>\n<p><code>Copy</code> values</p>\n</li>\n<li>\n<p><code>&amp;T</code> references</p>\n</li>\n<li>\n<p><code>&amp;mut T</code> references</p>\n</li>\n<li>\n<p>functions that change a value instead of returning a new one</p>\n</li>\n<li>\n<p>when borrows stop being used</p>\n</li>\n<li>\n<p>why Rust prevents conflicting access</p>\n</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is what our attack says.</p>\n<p>We started with:</p>\n<pre><code class=\"language-rust\">enemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>We ended with:</p>\n<pre><code class=\"language-rust\">attack(&amp;player, &amp;mut enemy);\n</code></pre>\n<p>The player is read.</p>\n<p>The enemy is changed.</p>\n<p>Both stay where they belong.</p>\n<p>And the compiler now checks that <code>attack()</code> keeps its side of that deal.</p>\n<hr />\n<h2 id=\"there-is-a-bigger-shape-missing\">There is a bigger shape missing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#there-is-a-bigger-shape-missing\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at what <code>main()</code> is holding now:</p>\n<pre><code class=\"language-text\">player\nenemy\nthe combat loop\n</code></pre>\n<p>That is a fight.</p>\n<p>It is not a game.</p>\n<p>When the Training Slime dies, the program simply ends.</p>\n<p>There is no next enemy.</p>\n<p>There is no stage counter.</p>\n<p>Nobody earns anything for winning.</p>\n<p>And every one of those things would become another loose variable inside <code>main()</code>:</p>\n<pre><code class=\"language-text\">player\nenemy\nstage\ngold\nexperience\n</code></pre>\n<p>We have seen this problem before.</p>\n<p>Three numbers were not a player.</p>\n<p>A pile of variables in <code>main()</code> is not a game.</p>\n<p>The pieces that make up our game need one place to live, and one place that decides what happens when a player clicks.</p>\n<p>Next, we will put the game in one place.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/borrowing-without-the-lecture/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Borrowing Without the Lecture.\nCanonical: https://mnixblog.netlify.app/borrowing-without-the-lecture/\nLet a Player Attack an Enemy in Rust\nOur game has a Player and an Enemy\nThen, the moment they fight, we strip them back down for parts:\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nWe built two real game types.\nThe moment they interact, we go back to passing loose numbers around.\nIn this article, we are going to change attack() so it takes the player and the enemy themselves.\nAlong the way, Rust will refuse to compile our code three times.\nEvery time, it will be right.\nBy the end, our combat loop will say what the game actually does:\nattack(&player, &mut enemy);\nA player attacks an enemy.\nNothing gets torn apart.\nOur starting point\nYou need two structs and four small functions to follow along:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nAt the bottom of main.rs, the tests module holds three tests for the small rules and one test that calls attack() directly:\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\nWe will come back to that test once attack() changes.\nOur combat loop looks like this:\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nIt works.\nBut attack() does not know what a player is.\nIt does not know what an enemy is.\nIt takes three numbers and hands back a fourth, and main() has to put that number back where it belongs.\nWhat we want to write is closer to:\nattack(player, enemy);\nSo let us try that.\nHand the player to attack()\nLet us start with the player and leave the enemy alone for a moment.\nChange attack() so it takes a whole Player instead of two loose damage values:\nfn attack(\nplayer: Player,\nhealth: u32,\n) -> u32 {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nhealth_after_hit(health, damage)\n}\nThe body barely changed.\nWe read the damage values from the player with the same dot syntax we used in main().\nNow update the call inside the loop:\nenemy.health =\nattack(player, enemy.health);\nThat reads much better.\nRun it:\ncargo run\nRust refuses to build the program.\nTrimmed down, the error says:\nerror[E0382]: use of moved value: `player`\nvalue moved here, in previous\niteration of loop\nThe full message is longer, and it suggests a few fixes, like cloning the player.\nSome of them would work.\nNone of them is what we want, so let us figure out what actually happened.\nThe first attack would have been fine.\nThe second attack is the problem.\nWhere did the player go?\nThe player moved\nIn Rust, every value has an owner.\nRight now, main() owns player.\nIt created it, so it is responsible for it.\nWhen we write:\nattack(player, enemy.health)\nand attack() asks for a Player, we are not lending the player to attack().\nWe are giving it away.\nOwnership moves into attack().\nWhen attack() finishes, the Player it owns is no longer available back in main().\nSo on the next trip around the loop, main() tries to hand over a player it no longer owns.\nRust catches that before the program ever runs.\nIn game terms, our player attacked once and then vanished.\nThat is a bold balancing decision.\nIt is not ours.\nThe attack was supposed to defeat the enemy.\nNot the player.\nWhy did health never move?\nWe have been passing enemy.health into functions all along, and it never disappeared.\nThat is because u32 implements Rust's Copy trait.\nWhen we pass a u32 by value, Rust can automatically make a copy, so the original remains available.\nOur Player does not implement Copy, so passing it by value moves ownership instead.\nYou do not need to understand traits yet.\nFor now, the useful difference is:\nu32     → copied\nPlayer  → moved\nA u32 gets photocopied.\nA Player gets handed over.\nWe do not want to give the player away.\nWe want attack() to look at it.\nTry it: this is the program with attack() taking a whole Player, and it fails to compile on purpose.\nRun it and read the error for yourself.\nYou should see error code E0382, \"use of moved value\", pointing at the call inside the loop.\nLive example · read the move error\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(player: Player, health: u32) -> u32 {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(player, enemy.health);\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/).\nThe next section fixes it with one character in two places.\nIf you want to fix it first, ask yourself what attack() needs to do with the player.\nQuestion: After attack(player, enemy.health) runs once, why can main() still use enemy.health but not player?\nFields of a struct are never moved\nu32 implements Copy, so a copy was passed, but Player does not, so it moved\nRust only tracks moves for values that contain a String\nAnswer: 2. u32 implements Copy, so a copy was passed, but Player does not, so it moved\nExplanation: Passing a u32 by value copies it. Player is not Copy, so passing it by value hands ownership to attack().\nBorrow the player with &\nRust lets a function borrow a value instead of owning it.\nAdd one character to the parameter type:\nfn attack(\nplayer: &Player,\nhealth: u32,\n) -> u32 {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nhealth_after_hit(health, damage)\n}\n&Player means:\na reference to a Player that someone else owns\nA reference lets attack() reach the player without taking ownership of it.\nWhen the call finishes, the player is still sitting in main() where we left it.\nNotice that the body did not change.\nThe dot works the same way through a reference, so:\nplayer.base_damage\nstill reads the field.\nThe call needs the same &:\nenemy.health =\nattack(&player, enemy.health);\n&player says:\nlet attack() borrow the player for this call\nRun it:\ncargo run\nThe Training Slime goes down in three hits again.\nThe player survives every one of them.\nThere is a bonus hiding in that &.\nA plain & reference gives attack() read-only access to the player's fields.\nIf attack() tried to assign a new value to:\nplayer.base_damage\nRust would refuse to compile it.\nThat is exactly the promise we want.\nAn attack needs to read the player.\nIt does not need to change the player.\nQuestion: Inside fn attack(player: &Player, ...), what does the line player.base_damage = 9; do?\nChanges the caller's player, since a reference points at it\nFails to compile, because a plain & reference is read-only\nCompiles, but only changes a private copy\nAnswer: 2. Fails to compile, because a plain & reference is read-only\nExplanation: Rust rejects it with E0594. The reference lets attack() read the player, not write to it.\nNow the enemy\nThe player is solved.\nThe enemy is still split apart:\nenemy.health =\nattack(&player, enemy.health);\nattack() gets a copy of the enemy's health, calculates a new number, and returns it.\nThen main() writes that number back into the enemy.\nWhat we really want is for attack() to change the enemy itself.\nWe just learned that borrowing keeps a value where it is.\nLet us borrow the enemy too:\nfn attack(\nplayer: &Player,\nenemy: &Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nTwo things changed besides the enemy parameter.\nThere is no:\n-> u32\nanymore.\nattack() is not handing back a number for someone else to store.\nThe result lives in the enemy.\nInside the function, we assign the new health directly to:\nenemy.health\nUpdate the call in main():\nattack(&player, &enemy);\nRun it:\ncargo run\nRust refuses again:\nerror[E0594]: cannot assign to\n`enemy.health`, which is behind\na `&` reference\nYou may also see a warning that enemy does not need to be mutable.\nIgnore it for a moment.\nWe are about to need that mut again.\nThis is the promise from the last section working against us.\n&Enemy lets us look at the enemy.\nWe did not come here to look.\nA plain & reference is read-only.\nThat was perfect for the player.\nIt is wrong for the enemy because the entire point of an attack is that the enemy's health changes.\nBorrow the enemy with &mut\nWe have already met mut.\nIt is how we told Rust that our enemy is allowed to change.\nReferences can express the same idea.\nChange:\n&Enemy\nto:\n&mut Enemy\nOur attack becomes:\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\n&mut Enemy means:\na mutable reference to an Enemy that someone else owns\nIn game terms:\nattack() may temporarily change this enemy, but it does not own the enemy.\nThe call needs to ask for the same thing:\nattack(&player, &mut enemy);\nThis only works because enemy was already declared with:\nlet mut enemy\nRust will not let us borrow something mutably if the owner never agreed that it could change.\nRun it:\ncargo run\nYou should see:\nTraining Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\nThe same fight as before.\nBut now the signature tells the whole story:\n&Player     → attack reads the player\n&mut Enemy  → attack changes the enemy\nNobody gets consumed.\nNobody gets torn apart.\nBoth are still in main(), ready for the next attack.\nTry it: here is a small version of the fixed program that attacks twice.\nRun it first.\nYou should see Training Slime health: 13, then Training Slime health: 6.\nThen break it on purpose, one change at a time, and read each error:\nchange enemy: &mut Enemy to enemy: &Enemy in attack() and you get E0594, the same error as before\nput enemy: &mut Enemy back, then remove mut from let mut enemy and you get E0596\nLive example · try breaking the borrow\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nattack(&player, &mut enemy);\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\nattack(&player, &mut enemy);\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/).\nQuestion: Why does attack(&player, &mut enemy) need let mut enemy?\nBecause &mut makes Rust copy the enemy first\nBecause Rust will not lend out a mutable borrow of a value its owner never declared changeable\nIt does not; &mut enemy works on any variable\nAnswer: 2. Because Rust will not lend out a mutable borrow of a value its owner never declared changeable\nExplanation: Without mut on the variable, the borrow fails with E0596.\nOne rule worth knowing now\nThere is one borrowing rule you will run into early, so here it is in game terms.\nSuppose that, instead of the combat loop, main() grabs a reference to the enemy's name before attacking:\nlet name = &enemy.name;\nattack(&player, &mut enemy);\nprintln!(\"{name} took a hit!\");\nRust refuses for the third time:\nerror[E0502]: cannot borrow `enemy`\nas mutable because it is also\nborrowed as immutable\nA borrow remains active until its last use.\nname is used by the println!() after the attack, so that borrow is still active when attack() asks for permission to change the enemy.\nRust will not hand out a mutable borrow while that reading borrow is still being used.\nOur combat loop never hits this problem because each borrow is finished before the next conflicting access begins:\nattack(&player, &mut enemy);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\nIn this case, the mutable borrow is finished when attack() returns.\nThen println!() is free to read the enemy.\nThat is the beginner version of the rule:\nFor the same data at the same time: many readers, or one writer, but not both.\nYou do not need more than that today.\nQuestion: Why does Rust reject reading name = &enemy.name before attack(&player, &mut enemy) when name is printed afterward?\nA & borrow can never be followed by a &mut borrow of the same value\nThe & borrow is still in use when attack() asks for &mut, so a reader and a writer overlap\nattack() takes ownership of enemy\nAnswer: 2. The & borrow is still in use when attack() asks for &mut, so a reader and a writer overlap\nExplanation: A borrow lasts until its last use. If nothing used name after the attack, the program would compile.\nCoding challenge: End the borrow in time\nThis starter fails to compile on purpose, because the lesson here is the error.\nFix it without changing attack(). Keep the names attack and main and the shape of Player and Enemy, because the checks call them. Press Check my code and it will tell you how you are doing.\nExpected result: it compiles and prints Training Slime took a hit! Health: 13.\nFix the borrow conflict\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(player: &Player, enemy: &mut Enemy) {\nlet damage = click_damage(player.base_damage, player.bonus_damage);\nenemy.health = health_after_hit(enemy.health, damage);\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\n// TODO: this borrow is still in use after the attack. Make the program compile.\nlet name = &enemy.name;\nattack(&player, &mut enemy);\nprintln!(\"{name} took a hit! Health: {}\", enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// It compiles now, and the program still runs from start to finish.\n#[test]\nfn main_still_runs() {\nmain();\n}\n/// It compiles, but attack() no longer does its job. We only wanted to move the borrow, not the damage.\n#[test]\nfn attack_still_lands() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 13);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/).\nTip:\nThe error is E0502. A borrow ends at its last use, so ask where name needs to start borrowing.\nUpdate the test\nOur attack() test still passes three loose numbers, so it needs the new signature:\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\nThe rule it checks has not changed.\nAn attack with five base damage and two bonus damage takes ten health down to three.\nWhat changed is how the test says it.\nIt builds a player.\nIt builds an enemy.\nIt lets one attack the other.\nThen it checks the enemy afterward.\nThat reads like the game.\nThe other three tests do not touch attack(), so they stay exactly as they were.\nRun them:\ncargo test\nAll four should pass.\nWe do not need a separate test proving that attack() does not change the player.\nWith our current Player type, an &Player reference does not allow attack() to assign new values to the player's fields.\nThe compiler checks that for us every time we build.\nThe complete program\nIf you have been following along in your editor, here is everything in one place.\nAt this point, src/main.rs should look like this:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nplayer: &Player,\nenemy: &mut Enemy,\n) {\nlet damage = click_damage(\nplayer.base_damage,\nplayer.bonus_damage,\n);\nenemy.health =\nhealth_after_hit(enemy.health, damage);\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nattack(&player, &mut enemy);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 10,\n};\nattack(&player, &mut enemy);\nassert_eq!(enemy.health, 3);\n}\n}\nRun both:\ncargo run\ncargo test\nThe Training Slime should fall in three hits, and all four tests should pass.\nWhat we learned\nAlong the way, we encountered:\nownership\nmoves\nCopy values\n&T references\n&mut T references\nfunctions that change a value instead of returning a new one\nwhen borrows stop being used\nwhy Rust prevents conflicting access\nYou do not need to memorize all of that yet.\nThe important change is what our attack says.\nWe started with:\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nWe ended with:\nattack(&player, &mut enemy);\nThe player is read.\nThe enemy is changed.\nBoth stay where they belong.\nAnd the compiler now checks that attack() keeps its side of that deal.\nThere is a bigger shape missing\nLook at what main() is holding now:\nplayer\nenemy\nthe combat loop\nThat is a fight.\nIt is not a game.\nWhen the Training Slime dies, the program simply ends.\nThere is no next enemy.\nThere is no stage counter.\nNobody earns anything for winning.\nAnd every one of those things would become another loose variable inside main():\nplayer\nenemy\nstage\ngold\nexperience\nWe have seen this problem before.\nThree numbers were not a player.\nA pile of variables in main() is not a game.\nThe pieces that make up our game need one place to live, and one place that decides what happens when a player clicks.\nNext, we will put the game in one place.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/borrowing-without-the-lecture/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-16T01:15:30Z",
      "id": "https://mnixblog.netlify.app/borrowing-without-the-lecture/",
      "image": "https://mnixblog.netlify.app/images/borrowing-without-the-lecture/cover-v2.jpg",
      "summary": "Learn Rust ownership and borrowing by fixing three real compiler errors. Let a player attack an enemy with &Player and &mut Enemy, without a lecture.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Borrowing Without the Lecture.",
      "url": "https://mnixblog.netlify.app/borrowing-without-the-lecture/"
    },
    {
      "content_html": "<p>Look at this line:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>Three values. Two belong to the player. One belongs to the enemy.</p>\n<p>Nothing in the code says which is which.</p>\n<p>In this article, we are going to fix that by grouping those values into two new Rust types: a <code>Player</code> and an <code>Enemy</code>.</p>\n<p>By the end, our code will start describing the things that actually exist in our game instead of asking us to remember what those numbers represent.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>You only need four small functions to follow along:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>Our combat loop currently looks like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health =\n            attack(enemy_health, 5, 2);\n\n        println!(\n            \"Enemy health: {enemy_health}\"\n        );\n    }\n\n    println!(\"Enemy defeated!\");\n}\n</code></pre>\n<p>This code works.</p>\n<p>The problem is not what it does.</p>\n<p>The problem is what it says.</p>\n<hr />\n<h2 id=\"three-numbers-with-three-different-meanings\">Three numbers with three different meanings<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#three-numbers-with-three-different-meanings\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at the attack again:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>The first value represents the enemy.</p>\n<p>The other two belong to the player.</p>\n<p>But nothing in the function call makes those relationships clear.</p>\n<p>Someone reading this code has to remember:</p>\n<pre><code class=\"language-text\">enemy_health → the enemy's current health\n5            → the player's base damage\n2            → the player's bonus damage\n</code></pre>\n<p>That is manageable today.</p>\n<p>It will become harder when our player also has:</p>\n<ul>\n<li>\n<p>a level</p>\n</li>\n<li>\n<p>experience</p>\n</li>\n<li>\n<p>gold</p>\n</li>\n<li>\n<p>upgrade levels</p>\n</li>\n<li>\n<p>passive damage</p>\n</li>\n</ul>\n<p>The enemy will need more information too:</p>\n<ul>\n<li>\n<p>a name</p>\n</li>\n<li>\n<p>maximum health</p>\n</li>\n<li>\n<p>gold rewards</p>\n</li>\n<li>\n<p>experience rewards</p>\n</li>\n</ul>\n<p>Passing every single value as an individual number just isn't going to scale.</p>\n<p>We need a way to tell Rust which values belong together.</p>\n<hr />\n<h2 id=\"give-the-player-some-shape\">Give the player some shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#give-the-player-some-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust lets us create our own types with <code>struct</code>.</p>\n<p>Add this near the top of <code>main.rs</code>:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n</code></pre>\n<p>A <strong>struct</strong> is a type that groups related values together.</p>\n<p>Our new type is called:</p>\n<pre><code class=\"language-text\">Player\n</code></pre>\n<p>It contains two named values:</p>\n<pre><code class=\"language-text\">base_damage\nbonus_damage\n</code></pre>\n<p>The named values inside a struct are called <strong>fields</strong>.</p>\n<p>Each field has a type. The colon syntax is the same as a function parameter—it connects the name to the type:</p>\n<pre><code class=\"language-rust\">base_damage: u32\nbonus_damage: u32\n</code></pre>\n<p>So every <code>Player</code> has a field named <code>base_damage</code> that contains a <code>u32</code>, and a field named <code>bonus_damage</code> that also contains a <code>u32</code>.</p>\n<p>The struct definition describes the shape of a player.</p>\n<p>It does not create one yet.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>After <code>struct Player { base_damage: u32, bonus_damage: u32 }</code>, how many players exist in the running program?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>One, with both damage fields set to <code>0</code></li>\n<li>None, because the definition only describes the shape of a player</li>\n<li>One, and it is mutable</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. None, because the definition only describes the shape of a player</p>\n<p>A <code>struct</code> definition is a blueprint. A player exists only once we write a value such as <code>Player { base_damage: 5, bonus_damage: 2 }</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"create-a-player\">Create a player<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#create-a-player\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now that Rust knows what a <code>Player</code> looks like, we can create one:</p>\n<pre><code class=\"language-rust\">let player = Player {\n    base_damage: 5,\n    bonus_damage: 2,\n};\n</code></pre>\n<p>This creates a value with a type of: <code>Player</code>.</p>\n<p>The values line up with the fields from our definition:</p>\n<pre><code class=\"language-text\">Player\n├── base_damage: 5\n└── bonus_damage: 2\n</code></pre>\n<p>We can access a field with a dot:</p>\n<pre><code class=\"language-rust\">player.base_damage\n</code></pre>\n<p>That gives us the player's base damage.</p>\n<p>We can do the same for bonus damage:</p>\n<pre><code class=\"language-rust\">player.bonus_damage\n</code></pre>\n<p>Our attack call can now become:</p>\n<pre><code class=\"language-rust\">attack(\n    enemy_health,\n    player.base_damage,\n    player.bonus_damage,\n)\n</code></pre>\n<p>That does happen to be longer than:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>But it is also clearer.</p>\n<p>We no longer have to remember what <code>5</code> and <code>2</code> represent.</p>\n<p>The code tells us.</p>\n<p>Try it: this builds a <code>Player</code> and attacks an enemy with 20 health. It should print <code>Enemy health: 13</code>. Change <code>bonus_damage</code> to <code>10</code> and predict the new number, then add a third field of your own to <code>Player</code> and see what the compiler asks of you.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the player</figcaption><pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let enemy_health = 20;\n    let remaining = attack(\n        enemy_health,\n        player.base_damage,\n        player.bonus_damage,\n    );\n\n    println!(&quot;Enemy health: {remaining}&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/\">the original article</a>.</p></figure>\n<hr />\n<h2 id=\"give-the-enemy-some-shape\">Give the enemy some shape<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#give-the-enemy-some-shape\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our enemy is still represented by one loose variable:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n</code></pre>\n<p>Health is not the only thing an enemy needs.</p>\n<p>Even giving it a name creates another related value.</p>\n<p>Add another struct:</p>\n<pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n}\n</code></pre>\n<p>Our new <code>Enemy</code> type contains two fields:</p>\n<pre><code class=\"language-text\">Enemy\n├── name\n└── health\n</code></pre>\n<p>We already know the type of <code>health</code>:</p>\n<pre><code class=\"language-rust\">health: u32\n</code></pre>\n<p>The name introduces a new type:</p>\n<pre><code class=\"language-rust\">name: String\n</code></pre>\n<hr />\n<h2 id=\"give-the-enemy-a-name\">Give the enemy a name<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#give-the-enemy-a-name\" aria-label=\"Link to this section\">#</a></h2>\n<p><code>String</code> is a Rust type for storing owned text.</p>\n<p>That fits our enemy: it should keep its own name for as long as it exists.</p>\n<p>We can create a <code>String</code> from text like this:</p>\n<pre><code class=\"language-rust\">String::from(\"Training Slime\")\n</code></pre>\n<p>For now, read that as:</p>\n<blockquote>\n<p>Create a <code>String</code> containing \"Training Slime.\"</p>\n</blockquote>\n<p>Our game only needs enough text to name an enemy.</p>\n<p>Now we can create one:</p>\n<pre><code class=\"language-rust\">let enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n</code></pre>\n<p>Rust now understands that the name and health belong to the same enemy.</p>\n<hr />\n<h2 id=\"the-enemy-still-needs-to-change\">The enemy still needs to change<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#the-enemy-still-needs-to-change\" aria-label=\"Link to this section\">#</a></h2>\n<p>There is one problem with our new enemy:</p>\n<pre><code class=\"language-rust\">let enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n</code></pre>\n<p>Combat changes health.</p>\n<p>But <code>enemy</code> is immutable by default.</p>\n<p>If we try to assign a new value to:</p>\n<pre><code class=\"language-rust\">enemy.health\n</code></pre>\n<p>Rust will refuse to compile the program.</p>\n<p>We have already solved this problem once.</p>\n<p>The enemy needs to be mutable:</p>\n<pre><code class=\"language-rust\">let mut enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n</code></pre>\n<p>The <code>mut</code> applies to the <code>enemy</code> value, which allows us to change its fields:</p>\n<pre><code class=\"language-rust\">enemy.health = 13;\n</code></pre>\n<p>We are not adding mutability everywhere.</p>\n<p>The player does not need to change yet:</p>\n<pre><code class=\"language-rust\">let player = Player {\n</code></pre>\n<p>The enemy does:</p>\n<pre><code class=\"language-rust\">let mut enemy = Enemy {\n</code></pre>\n<p>Once again, the game tells us which values need to be mutable.</p>\n<p>Try it: this program changes an enemy's health. It should print <code>Training Slime health: 13</code>. Now delete the word <code>mut</code> and run it again. Rust refuses to compile it and tells you it cannot assign to <code>enemy.health</code>, because <code>enemy</code> is not declared as mutable.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try removing mut</figcaption><pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn main() {\n    let mut enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n    };\n\n    enemy.health = 13;\n\n    println!(&quot;{} health: {}&quot;, enemy.name, enemy.health);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does the combat code write <code>let mut enemy</code> but plain <code>let player</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust requires <code>mut</code> on whichever struct is declared second</li>\n<li>Structs holding a <code>String</code> must always be <code>mut</code></li>\n<li>Combat changes <code>enemy.health</code>, while nothing changes the player</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. Combat changes <code>enemy.health</code>, while nothing changes the player</p>\n<p>Values are immutable by default. We add <code>mut</code> only where the game actually reassigns a field, which is the enemy's health.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"put-the-player-and-enemy-into-combat\">Put the player and enemy into combat<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#put-the-player-and-enemy-into-combat\" aria-label=\"Link to this section\">#</a></h2>\n<p>We can now update <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(\n            enemy.health,\n            player.base_damage,\n            player.bonus_damage,\n        );\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n</code></pre>\n<p>There is one small change in our <code>println!()</code> calls.</p>\n<p>The <code>{}</code> placeholders are filled from left to right by the values that follow the format string:</p>\n<pre><code class=\"language-rust\">println!(\n    \"{} health: {}\",\n    enemy.name,\n    enemy.health\n);\n</code></pre>\n<p>The first <code>{}</code> displays <code>enemy.name</code>.</p>\n<p>The second displays <code>enemy.health</code>.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In <code>println!(\"{} health: {}\", enemy.name, enemy.health);</code>, which value fills the first <code>{}</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>enemy.name</code>, because the placeholders are filled left to right</li>\n<li><code>enemy.health</code>, because numbers are always printed first</li>\n<li>Whichever field is declared first in the <code>Enemy</code> struct</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. <code>enemy.name</code>, because the placeholders are filled left to right</p>\n<p>The values after the format string fill the <code>{}</code> placeholders in the order you write them, not in the order the struct declares its fields.</p>\n</div>\n</div>\n</section>\n<p>Run the game:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Training Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\n</code></pre>\n<p>The combat still works.</p>\n<p>But the code now contains an actual player and enemy.</p>\n<p><strong>Coding challenge: Pay the player</strong></p>\n<p>Enemies should be worth something. Add a <code>gold_reward: u32</code> field to <code>Enemy</code>, give the Training Slime a reward of <code>5</code>, and print it using the field instead of the placeholder <code>0</code>. Keep the field name <code>gold_reward</code>, and press <strong>Check my code</strong> to see how you are doing.</p>\n<p><strong>Expected result:</strong> the program prints <code>Training Slime is worth 5 gold</code>. As written, the starter prints <code>Training Slime is worth 0 gold</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Give the enemy a reward</figcaption><pre><code class=\"language-rust\">struct Enemy {\n    name: String,\n    health: u32,\n    // TODO: add a gold_reward field of type u32\n}\n\nfn main() {\n    let enemy = Enemy {\n        name: String::from(&quot;Training Slime&quot;),\n        health: 20,\n        // TODO: give the Training Slime a gold_reward of 5\n    };\n\n    // TODO: replace the 0 with enemy.gold_reward\n    println!(&quot;{} is worth {} gold&quot;, enemy.name, 0);\n    println!(&quot;It has {} health&quot;, enemy.health);\n}\n\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// `Enemy` needs a field called `gold_reward`, and it should hold a `u32`. Check the name and the type.\n#[test]\nfn enemy_has_a_u32_gold_reward() {\n    let enemy = Enemy {\n        name: String::from(&quot;Test Slime&quot;),\n        health: 1,\n        gold_reward: 7u32,\n    };\n    let reward: u32 = enemy.gold_reward;\n    assert_eq!(reward, 7);\n}\n\n/// Adding a field should not cost us the old ones. `name` and `health` still belong on `Enemy`.\n#[test]\nfn old_fields_survive() {\n    let enemy = Enemy {\n        name: String::from(&quot;Test Slime&quot;),\n        health: 9,\n        gold_reward: 1,\n    };\n    assert_eq!(enemy.name, &quot;Test Slime&quot;);\n    assert_eq!(enemy.health, 9);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>A new field needs two edits: its name and type in the <code>struct</code> definition, and a value wherever you create an <code>Enemy</code>. Then read it with a dot, like <code>enemy.health</code>.</p>\n</aside>\n<hr />\n<h2 id=\"notice-what-the-structs-made-visible\">Notice what the structs made visible<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#notice-what-the-structs-made-visible\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before, our game looked like this:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n\nenemy_health =\n    attack(enemy_health, 5, 2);\n</code></pre>\n<p>Now it looks like this:</p>\n<pre><code class=\"language-rust\">let player = Player {\n    base_damage: 5,\n    bonus_damage: 2,\n};\n\nlet mut enemy = Enemy {\n    name: String::from(\"Training Slime\"),\n    health: 20,\n};\n\nenemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>The behavior is the same.</p>\n<p>The meaning is not.</p>\n<p>Before:</p>\n<pre><code class=\"language-text\">20\n5\n2\n</code></pre>\n<p>Now:</p>\n<pre><code class=\"language-text\">Player\n├── base_damage\n└── bonus_damage\n\nEnemy\n├── name\n└── health\n</code></pre>\n<p>We gave related values names and grouped them into types.</p>\n<p>Rust can now help us keep player data with the player and enemy data with the enemy.</p>\n<hr />\n<h2 id=\"the-behavior-did-not-change\">The behavior did not change<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#the-behavior-did-not-change\" aria-label=\"Link to this section\">#</a></h2>\n<p>Notice what we did not touch:</p>\n<pre><code class=\"language-rust\">click_damage()\n</code></pre>\n<p>still calculates damage.</p>\n<pre><code class=\"language-rust\">health_after_hit()\n</code></pre>\n<p>still reduces health.</p>\n<pre><code class=\"language-rust\">is_defeated()\n</code></pre>\n<p>still checks for defeat.</p>\n<pre><code class=\"language-rust\">attack()\n</code></pre>\n<p>still combines the smaller rules.</p>\n<p>We changed how the application organizes its data.</p>\n<p>We did not rewrite the behavior we already trust.</p>\n<p>Run the tests:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>All four should still pass.</p>\n<p>No new combat rule was added in this article, so we do not need to invent a new behavior test just to prove that a struct can hold the values we typed into it.</p>\n<p>The compiler checks that our struct fields exist and contain the expected types.</p>\n<p>Our existing tests continue checking the game rules.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">struct Player {\n    base_damage: u32,\n    bonus_damage: u32,\n}\n\nstruct Enemy {\n    name: String,\n    health: u32,\n}\n\nfn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let player = Player {\n        base_damage: 5,\n        bonus_damage: 2,\n    };\n\n    let mut enemy = Enemy {\n        name: String::from(\"Training Slime\"),\n        health: 20,\n    };\n\n    while !is_defeated(enemy.health) {\n        enemy.health = attack(\n            enemy.health,\n            player.base_damage,\n            player.bonus_damage,\n        );\n\n        println!(\n            \"{} health: {}\",\n            enemy.name,\n            enemy.health\n        );\n    }\n\n    println!(\"{} defeated!\", enemy.name);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        assert_eq!(attack(10, 5, 2), 3);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>The game should defeat the Training Slime, and all four tests should pass.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>\n<p><code>struct</code></p>\n</li>\n<li>\n<p>custom Rust types</p>\n</li>\n<li>\n<p>struct fields</p>\n</li>\n<li>\n<p>creating struct values</p>\n</li>\n<li>\n<p>field access with <code>.</code></p>\n</li>\n<li>\n<p><code>String</code></p>\n</li>\n<li>\n<p>mutable struct values</p>\n</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is what happened to our data.</p>\n<p>We started with:</p>\n<pre><code class=\"language-text\">enemy_health\n5\n2\n</code></pre>\n<p>We turned those values into:</p>\n<pre><code class=\"language-text\">Player\nEnemy\n</code></pre>\n<p>Our program now has a vocabulary that matches the game we are building.</p>\n<hr />\n<h2 id=\"there-is-still-something-awkward\">There is still something awkward<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#there-is-still-something-awkward\" aria-label=\"Link to this section\">#</a></h2>\n<p>We solved one problem.</p>\n<p>But look at our attack again:</p>\n<pre><code class=\"language-rust\">enemy.health = attack(\n    enemy.health,\n    player.base_damage,\n    player.bonus_damage,\n);\n</code></pre>\n<p>We created a <code>Player</code>.</p>\n<p>We created an <code>Enemy</code>.</p>\n<p>Then, the moment they need to interact, we pull them apart into individual fields.</p>\n<p><code>attack()</code> does not accept a player.</p>\n<p>It does not accept an enemy.</p>\n<p>It still accepts three numbers and returns another number that we manually assign back to the enemy.</p>\n<p>What we really want to express is:</p>\n<pre><code class=\"language-text\">player attacks enemy\n</code></pre>\n<p>That means our attack needs to:</p>\n<pre><code class=\"language-text\">read the player\nchange the enemy\nleave both available for the next attack\n</code></pre>\n<p>Rust has a way to let a function temporarily use values without taking them away from the code that already owns them.</p>\n<p>That is called <strong>borrowing</strong>.</p>\n<p>Next, we will let our player attack an enemy without consuming either one.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/three-numbers-are-not-a-player/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Three Numbers Are Not a Player.\nCanonical: https://mnixblog.netlify.app/three-numbers-are-not-a-player/\nGive Your Game Data Shape With Rust Structs\nLook at this line:\nattack(enemy_health, 5, 2)\nThree values. Two belong to the player. One belongs to the enemy.\nNothing in the code says which is which.\nIn this article, we are going to fix that by grouping those values into two new Rust types: a Player and an Enemy.\nBy the end, our code will start describing the things that actually exist in our game instead of asking us to remember what those numbers represent.\nOur starting point\nYou only need four small functions to follow along:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nOur combat loop currently looks like this:\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\n\"Enemy health: {enemy_health}\"\n);\n}\nprintln!(\"Enemy defeated!\");\n}\nThis code works.\nThe problem is not what it does.\nThe problem is what it says.\nThree numbers with three different meanings\nLook at the attack again:\nattack(enemy_health, 5, 2)\nThe first value represents the enemy.\nThe other two belong to the player.\nBut nothing in the function call makes those relationships clear.\nSomeone reading this code has to remember:\nenemy_health → the enemy's current health\n5            → the player's base damage\n2            → the player's bonus damage\nThat is manageable today.\nIt will become harder when our player also has:\na level\nexperience\ngold\nupgrade levels\npassive damage\nThe enemy will need more information too:\na name\nmaximum health\ngold rewards\nexperience rewards\nPassing every single value as an individual number just isn't going to scale.\nWe need a way to tell Rust which values belong together.\nGive the player some shape\nRust lets us create our own types with struct.\nAdd this near the top of main.rs:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nA struct is a type that groups related values together.\nOur new type is called:\nPlayer\nIt contains two named values:\nbase_damage\nbonus_damage\nThe named values inside a struct are called fields.\nEach field has a type. The colon syntax is the same as a function parameter—it connects the name to the type:\nbase_damage: u32\nbonus_damage: u32\nSo every Player has a field named base_damage that contains a u32, and a field named bonus_damage that also contains a u32.\nThe struct definition describes the shape of a player.\nIt does not create one yet.\nQuestion: After struct Player { base_damage: u32, bonus_damage: u32 }, how many players exist in the running program?\nOne, with both damage fields set to 0\nNone, because the definition only describes the shape of a player\nOne, and it is mutable\nAnswer: 2. None, because the definition only describes the shape of a player\nExplanation: A struct definition is a blueprint. A player exists only once we write a value such as Player { base_damage: 5, bonus_damage: 2 }.\nCreate a player\nNow that Rust knows what a Player looks like, we can create one:\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nThis creates a value with a type of: Player.\nThe values line up with the fields from our definition:\nPlayer\n├── base_damage: 5\n└── bonus_damage: 2\nWe can access a field with a dot:\nplayer.base_damage\nThat gives us the player's base damage.\nWe can do the same for bonus damage:\nplayer.bonus_damage\nOur attack call can now become:\nattack(\nenemy_health,\nplayer.base_damage,\nplayer.bonus_damage,\n)\nThat does happen to be longer than:\nattack(enemy_health, 5, 2)\nBut it is also clearer.\nWe no longer have to remember what 5 and 2 represent.\nThe code tells us.\nTry it: this builds a Player and attacks an enemy with 20 health. It should print Enemy health: 13. Change bonus_damage to 10 and predict the new number, then add a third field of your own to Player and see what the compiler asks of you.\nLive example · try changing the player\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet enemy_health = 20;\nlet remaining = attack(\nenemy_health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\"Enemy health: {remaining}\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/).\nGive the enemy some shape\nOur enemy is still represented by one loose variable:\nlet mut enemy_health = 20;\nHealth is not the only thing an enemy needs.\nEven giving it a name creates another related value.\nAdd another struct:\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nOur new Enemy type contains two fields:\nEnemy\n├── name\n└── health\nWe already know the type of health:\nhealth: u32\nThe name introduces a new type:\nname: String\nGive the enemy a name\nString is a Rust type for storing owned text.\nThat fits our enemy: it should keep its own name for as long as it exists.\nWe can create a String from text like this:\nString::from(\"Training Slime\")\nFor now, read that as:\nCreate a String containing \"Training Slime.\"\nOur game only needs enough text to name an enemy.\nNow we can create one:\nlet enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nRust now understands that the name and health belong to the same enemy.\nThe enemy still needs to change\nThere is one problem with our new enemy:\nlet enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nCombat changes health.\nBut enemy is immutable by default.\nIf we try to assign a new value to:\nenemy.health\nRust will refuse to compile the program.\nWe have already solved this problem once.\nThe enemy needs to be mutable:\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nThe mut applies to the enemy value, which allows us to change its fields:\nenemy.health = 13;\nWe are not adding mutability everywhere.\nThe player does not need to change yet:\nlet player = Player {\nThe enemy does:\nlet mut enemy = Enemy {\nOnce again, the game tells us which values need to be mutable.\nTry it: this program changes an enemy's health. It should print Training Slime health: 13. Now delete the word mut and run it again. Rust refuses to compile it and tells you it cannot assign to enemy.health, because enemy is not declared as mutable.\nLive example · try removing mut\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn main() {\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nenemy.health = 13;\nprintln!(\"{} health: {}\", enemy.name, enemy.health);\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/).\nQuestion: Why does the combat code write let mut enemy but plain let player?\nRust requires mut on whichever struct is declared second\nStructs holding a String must always be mut\nCombat changes enemy.health, while nothing changes the player\nAnswer: 3. Combat changes enemy.health, while nothing changes the player\nExplanation: Values are immutable by default. We add mut only where the game actually reassigns a field, which is the enemy's health.\nPut the player and enemy into combat\nWe can now update main():\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\nThere is one small change in our println!() calls.\nThe {} placeholders are filled from left to right by the values that follow the format string:\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\nThe first {} displays enemy.name.\nThe second displays enemy.health.\nQuestion: In println!(\"{} health: {}\", enemy.name, enemy.health);, which value fills the first {}?\nenemy.name, because the placeholders are filled left to right\nenemy.health, because numbers are always printed first\nWhichever field is declared first in the Enemy struct\nAnswer: 1. enemy.name, because the placeholders are filled left to right\nExplanation: The values after the format string fill the {} placeholders in the order you write them, not in the order the struct declares its fields.\nRun the game:\ncargo run\nYou should see:\nTraining Slime health: 13\nTraining Slime health: 6\nTraining Slime health: 0\nTraining Slime defeated!\nThe combat still works.\nBut the code now contains an actual player and enemy.\nCoding challenge: Pay the player\nEnemies should be worth something. Add a gold_reward: u32 field to Enemy, give the Training Slime a reward of 5, and print it using the field instead of the placeholder 0. Keep the field name gold_reward, and press Check my code to see how you are doing.\nExpected result: the program prints Training Slime is worth 5 gold. As written, the starter prints Training Slime is worth 0 gold.\nGive the enemy a reward\nstruct Enemy {\nname: String,\nhealth: u32,\n// TODO: add a gold_reward field of type u32\n}\nfn main() {\nlet enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n// TODO: give the Training Slime a gold_reward of 5\n};\n// TODO: replace the 0 with enemy.gold_reward\nprintln!(\"{} is worth {} gold\", enemy.name, 0);\nprintln!(\"It has {} health\", enemy.health);\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// `Enemy` needs a field called `gold_reward`, and it should hold a `u32`. Check the name and the type.\n#[test]\nfn enemy_has_a_u32_gold_reward() {\nlet enemy = Enemy {\nname: String::from(\"Test Slime\"),\nhealth: 1,\ngold_reward: 7u32,\n};\nlet reward: u32 = enemy.gold_reward;\nassert_eq!(reward, 7);\n}\n/// Adding a field should not cost us the old ones. `name` and `health` still belong on `Enemy`.\n#[test]\nfn old_fields_survive() {\nlet enemy = Enemy {\nname: String::from(\"Test Slime\"),\nhealth: 9,\ngold_reward: 1,\n};\nassert_eq!(enemy.name, \"Test Slime\");\nassert_eq!(enemy.health, 9);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/).\nTip:\nA new field needs two edits: its name and type in the struct definition, and a value wherever you create an Enemy. Then read it with a dot, like enemy.health.\nNotice what the structs made visible\nBefore, our game looked like this:\nlet mut enemy_health = 20;\nenemy_health =\nattack(enemy_health, 5, 2);\nNow it looks like this:\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nThe behavior is the same.\nThe meaning is not.\nBefore:\n20\n5\n2\nNow:\nPlayer\n├── base_damage\n└── bonus_damage\nEnemy\n├── name\n└── health\nWe gave related values names and grouped them into types.\nRust can now help us keep player data with the player and enemy data with the enemy.\nThe behavior did not change\nNotice what we did not touch:\nclick_damage()\nstill calculates damage.\nhealth_after_hit()\nstill reduces health.\nis_defeated()\nstill checks for defeat.\nattack()\nstill combines the smaller rules.\nWe changed how the application organizes its data.\nWe did not rewrite the behavior we already trust.\nRun the tests:\ncargo test\nAll four should still pass.\nNo new combat rule was added in this article, so we do not need to invent a new behavior test just to prove that a struct can hold the values we typed into it.\nThe compiler checks that our struct fields exist and contain the expected types.\nOur existing tests continue checking the game rules.\nThe complete program\nAt this point, src/main.rs should look like this:\nstruct Player {\nbase_damage: u32,\nbonus_damage: u32,\n}\nstruct Enemy {\nname: String,\nhealth: u32,\n}\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet player = Player {\nbase_damage: 5,\nbonus_damage: 2,\n};\nlet mut enemy = Enemy {\nname: String::from(\"Training Slime\"),\nhealth: 20,\n};\nwhile !is_defeated(enemy.health) {\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nprintln!(\n\"{} health: {}\",\nenemy.name,\nenemy.health\n);\n}\nprintln!(\"{} defeated!\", enemy.name);\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\n}\nRun both:\ncargo run\ncargo test\nThe game should defeat the Training Slime, and all four tests should pass.\nWhat we learned\nAlong the way, we encountered:\nstruct\ncustom Rust types\nstruct fields\ncreating struct values\nfield access with .\nString\nmutable struct values\nYou do not need to memorize all of that yet.\nThe important change is what happened to our data.\nWe started with:\nenemy_health\n5\n2\nWe turned those values into:\nPlayer\nEnemy\nOur program now has a vocabulary that matches the game we are building.\nThere is still something awkward\nWe solved one problem.\nBut look at our attack again:\nenemy.health = attack(\nenemy.health,\nplayer.base_damage,\nplayer.bonus_damage,\n);\nWe created a Player.\nWe created an Enemy.\nThen, the moment they need to interact, we pull them apart into individual fields.\nattack() does not accept a player.\nIt does not accept an enemy.\nIt still accepts three numbers and returns another number that we manually assign back to the enemy.\nWhat we really want to express is:\nplayer attacks enemy\nThat means our attack needs to:\nread the player\nchange the enemy\nleave both available for the next attack\nRust has a way to let a function temporarily use values without taking them away from the code that already owns them.\nThat is called borrowing.\nNext, we will let our player attack an enemy without consuming either one.\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/three-numbers-are-not-a-player/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-14T02:06:53Z",
      "id": "https://mnixblog.netlify.app/three-numbers-are-not-a-player/",
      "image": "https://mnixblog.netlify.app/images/three-numbers-are-not-a-player/cover.jpg",
      "summary": "When your attack takes three loose u32 values, Rust has no idea which belong to the player. Learn how structs give your game data a name and a shape.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Three Numbers Are Not a Player.",
      "url": "https://mnixblog.netlify.app/three-numbers-are-not-a-player/"
    },
    {
      "content_html": "<p>Our game can now calculate damage, reduce health, and decide whether an enemy has been defeated.</p>\n<p>There is a problem, though.</p>\n<p>Those rules do not actually <strong>attack</strong> anything yet.</p>\n<p>In this article, we are going to combine our small Rust functions into a larger <code>attack()</code> function, let enemy health change during combat, and repeat attacks until the enemy is defeated.</p>\n<p>By the end, we will have our first actual game mechanic: a combat loop.</p>\n<hr />\n<h2 id=\"our-starting-point\">Our starting point<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#our-starting-point\" aria-label=\"Link to this section\">#</a></h2>\n<p>You only need three small functions to follow along:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n</code></pre>\n<p>Each one has a single job.</p>\n<p><code>click_damage()</code> calculates how much damage the player deals.</p>\n<p><code>health_after_hit()</code> applies that damage to an enemy's health.</p>\n<p><code>is_defeated()</code> tells us whether the enemy has reached zero health.</p>\n<p>Right now, however, something else has to coordinate all three.</p>\n<p>That something is <code>main()</code>.</p>\n<hr />\n<h2 id=\"main-knows-too-much\"><code>main()</code> knows too much<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#main-knows-too-much\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could attack an enemy like this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let enemy_health = 10;\n\n    let damage = click_damage(5, 2);\n\n    let remaining_health =\n        health_after_hit(enemy_health, damage);\n\n    println!(\"Enemy health: {remaining_health}\");\n\n    if is_defeated(remaining_health) {\n        println!(\"Enemy defeated!\");\n    }\n}\n</code></pre>\n<p>Technically, this does work.</p>\n<p>But look at everything that <code>main()</code> knows about.</p>\n<p>It knows how damage is calculated.</p>\n<p>It knows how damage affects health.</p>\n<p>It knows how to determine whether the enemy has been defeated.</p>\n<p>Those first two steps describe one larger idea:</p>\n<p><strong>an attack.</strong></p>\n<p>So let us give that idea a function.</p>\n<hr />\n<h2 id=\"build-an-attack-from-smaller-functions\">Build an attack from smaller functions<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#build-an-attack-from-smaller-functions\" aria-label=\"Link to this section\">#</a></h2>\n<p>Add this beneath the functions we already have:</p>\n<pre><code class=\"language-rust\">fn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n</code></pre>\n<p>Notice how we are not adding new Rust ideas here.</p>\n<p>That is the point.</p>\n<p>We are taking functions we already understand and using them to build something more complex.</p>\n<p>The first line inside <code>attack()</code>:</p>\n<pre><code class=\"language-rust\">let damage =\n    click_damage(base_damage, bonus_damage);\n</code></pre>\n<p>uses our existing damage rule.</p>\n<p>Then:</p>\n<pre><code class=\"language-rust\">health_after_hit(health, damage)\n</code></pre>\n<p>uses the result to calculate the enemy's remaining health.</p>\n<p>Because it is the final expression and has no semicolon, that remaining health becomes the value returned by <code>attack()</code>.</p>\n<p>Our functions now fit together like this:</p>\n<pre><code class=\"language-text\">click_damage() ───────┐\n                      ├──→ attack()\nhealth_after_hit() ───┘\n</code></pre>\n<p>We did not replace our small functions.</p>\n<p><strong>We composed a larger behavior from them.</strong></p>\n<p>Instead of teaching <code>attack()</code> every rule from scratch, it delegates those jobs to functions that already know how to calculate damage and reduce health.</p>\n<p>We already know what each piece does, so understanding <code>attack()</code> does not require us to understand an entirely new system.</p>\n<p>Try it: this runs <code>attack()</code> against an enemy with ten health. It should print <code>Enemy health: 3</code>. Change the bonus damage from <code>2</code> to <code>1000</code> and predict the result before you run it.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the bonus damage</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let remaining_health = attack(10, 5, 2);\n\n    println!(&quot;Enemy health: {remaining_health}&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>attack()</code> call <code>click_damage()</code> and <code>health_after_hit()</code> instead of repeating their arithmetic?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust does not allow arithmetic inside a function that returns <code>u32</code></li>\n<li>The damage and health rules stay in one place each, so a change there changes every attack</li>\n<li>Calling a function is always faster than writing the arithmetic inline</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The damage and health rules stay in one place each, so a change there changes every attack</p>\n<p>Composition reuses the rules we already trust. Changing <code>click_damage()</code> later changes every attack without touching <code>attack()</code>.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"try-the-attack\">Try the attack<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#try-the-attack\" aria-label=\"Link to this section\">#</a></h2>\n<p>Replace <code>main()</code> with:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let enemy_health = 10;\n\n    let remaining_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {remaining_health}\");\n}\n</code></pre>\n<p>Run the game:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Enemy health: 3\n</code></pre>\n<p>Our player dealt seven damage to an enemy with ten health.</p>\n<p>That means <code>attack()</code> works.</p>\n<p>We can test it too.</p>\n<p>Add this to the test module:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    assert_eq!(attack(10, 5, 2), 3);\n}\n</code></pre>\n<p>Then run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>We now have a test for behavior that depends on several smaller rules working together.</p>\n<hr />\n<h2 id=\"one-attack-is-not-much-of-a-fight\">One attack is not much of a fight<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#one-attack-is-not-much-of-a-fight\" aria-label=\"Link to this section\">#</a></h2>\n<p>We can attack an enemy once.</p>\n<p>An idle game probably needs a little more combat than that.</p>\n<p>Suppose our enemy starts with twenty health:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n</code></pre>\n<p>The first attack reduces it to thirteen.</p>\n<p>The next should reduce it to six.</p>\n<p>The next should reduce it to zero.</p>\n<p>There is a problem, though.</p>\n<p>This:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n</code></pre>\n<p>creates a variable that Rust does not allow us to change.</p>\n<p>Variables are <strong>immutable by default</strong> in Rust.</p>\n<p>That means this is not allowed:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n\nenemy_health = attack(enemy_health, 5, 2);\n</code></pre>\n<p>If you try this, Rust will refuse to compile it and tell us that we cannot assign to an immutable variable.</p>\n<p>That error is useful. We have to explicitly tell Rust which values we expect to change.</p>\n<p>For most values, that is a useful default.</p>\n<p>For health during combat, it is not.</p>\n<p>Health is supposed to change.</p>\n<hr />\n<h2 id=\"let-health-change-with-mut\">Let health change with <code>mut</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#let-health-change-with-mut\" aria-label=\"Link to this section\">#</a></h2>\n<p>Rust uses the <code>mut</code> keyword when we want a variable to be mutable.</p>\n<p>Change:</p>\n<pre><code class=\"language-rust\">let enemy_health = 20;\n</code></pre>\n<p>to:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n</code></pre>\n<p>Now we can assign a new value to it:</p>\n<pre><code class=\"language-rust\">enemy_health =\n    attack(enemy_health, 5, 2);\n</code></pre>\n<p>The word <code>mut</code> is short for <strong>mutable</strong>, meaning the value bound to this variable may change.</p>\n<p>We are not making everything mutable just because we can.</p>\n<p>We are using mutability because enemy health represents something that changes as part of the game.</p>\n<p>That distinction will become increasingly useful as our state grows.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does the combat loop declare <code>let mut enemy_health = 20;</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>while</code> loops only work with variables declared <code>mut</code></li>\n<li><code>println!</code> needs a mutable variable to display it</li>\n<li>The loop assigns a new health value after every attack</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. The loop assigns a new health value after every attack</p>\n<p><code>mut</code> is needed because of the reassignment <code>enemy_health = attack(...)</code>, not because of the loop or the printing.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"attack-again\">Attack again<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#attack-again\" aria-label=\"Link to this section\">#</a></h2>\n<p>We can now do this:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut enemy_health = 20;\n\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n}\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>and we get:</p>\n<pre><code class=\"language-text\">Enemy health: 13\nEnemy health: 6\nEnemy health: 0\n</code></pre>\n<p>It works.</p>\n<p>It is also repetitive.</p>\n<p>We know what should happen:</p>\n<blockquote>\n<p>Keep attacking while the enemy is still alive.</p>\n</blockquote>\n<p>Rust has a way to express exactly that.</p>\n<hr />\n<h2 id=\"repeat-with-while\">Repeat with <code>while</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#repeat-with-while\" aria-label=\"Link to this section\">#</a></h2>\n<p>A <code>while</code> loop repeats a block of code as long as a condition remains <code>true</code>.</p>\n<p>For example:</p>\n<pre><code class=\"language-rust\">while enemy_health &gt; 0 {\n    // repeat something\n}\n</code></pre>\n<p>We could use that.</p>\n<p>But we already wrote a function that knows whether the enemy has been defeated:</p>\n<pre><code class=\"language-rust\">is_defeated(enemy_health)\n</code></pre>\n<p>We should use the rule we already have.</p>\n<p>What we really want to say is:</p>\n<blockquote>\n<p>Keep attacking while the enemy is <strong>not</strong> defeated.</p>\n</blockquote>\n<p>Rust uses <code>!</code> to mean <strong>not</strong>.</p>\n<p>So:</p>\n<pre><code class=\"language-rust\">!is_defeated(enemy_health)\n</code></pre>\n<p>means:</p>\n<blockquote>\n<p>the enemy has not been defeated</p>\n</blockquote>\n<p>Put that inside a <code>while</code> loop:</p>\n<pre><code class=\"language-rust\">while !is_defeated(enemy_health) {\n    enemy_health =\n        attack(enemy_health, 5, 2);\n\n    println!(\"Enemy health: {enemy_health}\");\n}\n</code></pre>\n<p>Now Rust keeps running the block until <code>is_defeated()</code> returns <code>true</code>.</p>\n<p>Try it: this is the whole loop. It should print <code>Enemy health: 13</code>, <code>Enemy health: 6</code>, <code>Enemy health: 0</code>, then <code>Enemy defeated!</code>. Change the starting health to <code>50</code> and predict how many lines of health you will see, then change it to <code>0</code> and predict again.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the starting health</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health = attack(enemy_health, 5, 2);\n\n        println!(&quot;Enemy health: {enemy_health}&quot;);\n    }\n\n    println!(&quot;Enemy defeated!&quot;);\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does the loop above print if <code>enemy_health</code> starts at <code>0</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>Enemy health: 0</code> and then <code>Enemy defeated!</code></li>\n<li>Only <code>Enemy defeated!</code></li>\n<li>Nothing, because the program fails to compile</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Only <code>Enemy defeated!</code></p>\n<p><code>while</code> checks its condition before each pass. <code>!is_defeated(0)</code> is already <code>false</code>, so the body never runs and only the final <code>println!</code> does.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"our-first-combat-loop\">Our first combat loop<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#our-first-combat-loop\" aria-label=\"Link to this section\">#</a></h2>\n<p>Update <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health =\n            attack(enemy_health, 5, 2);\n\n        println!(\n            \"Enemy health: {enemy_health}\"\n        );\n    }\n\n    println!(\"Enemy defeated!\");\n}\n</code></pre>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">Enemy health: 13\nEnemy health: 6\nEnemy health: 0\nEnemy defeated!\n</code></pre>\n<p>We have a combat loop.</p>\n<p>The player attacks.</p>\n<p>Health changes.</p>\n<p>The game checks whether combat should continue.</p>\n<p>Then it attacks again.</p>\n<p>Our little system now looks something like this:</p>\n<pre><code class=\"language-text\">click_damage() ───────┐\n                      ├──→ attack()\nhealth_after_hit() ───┘\n                      ↓\n                  repeat while\n          enemy has not been defeated\n</code></pre>\n<p>The individual pieces are still small.</p>\n<p>The behavior is getting larger.</p>\n<p><strong>Coding challenge: Count the swings</strong></p>\n<p>A fight is more fun when the game can say how long it took. Add a counter that goes up once for every attack, so the program reports how many swings it took to defeat the enemy. The loop now lives in a small <code>fight</code> function so the count can be handed back, and <strong>Check my code</strong> will tell you how you are doing. Keep the name and signature of <code>fight</code>.</p>\n<p><strong>Expected result:</strong> the program prints the three health lines, <code>Enemy defeated!</code>, and then <code>Swings: 3</code>. As written, the starter ends with <code>Swings: 0</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Count the swings</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let damage = click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn fight(mut enemy_health: u32, base_damage: u32, bonus_damage: u32) -&gt; u32 {\n    let mut swings = 0;\n\n    while !is_defeated(enemy_health) {\n        enemy_health = attack(enemy_health, base_damage, bonus_damage);\n        // TODO: add 1 to swings every time the player attacks\n\n        println!(&quot;Enemy health: {enemy_health}&quot;);\n    }\n\n    swings\n}\n\nfn main() {\n    let swings = fight(20, 5, 2);\n\n    println!(&quot;Enemy defeated!&quot;);\n    println!(&quot;Swings: {swings}&quot;);\n}\n\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// The fight should report how many attacks it took. Look at what `fight` hands back at the end.\n#[test]\nfn counts_every_swing() {\n    assert_eq!(fight(20, 5, 2), 3);\n}\n\n/// An enemy that falls to one hit still took a swing. Check what a short fight reports.\n#[test]\nfn one_hit_is_one_swing() {\n    assert_eq!(fight(7, 5, 2), 1);\n}\n\n/// A longer fight should report a longer count. The number has to come from the loop, not from us.\n#[test]\nfn long_fight_counts_more() {\n    assert_eq!(fight(70, 5, 2), 10);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p><code>swings</code> is <code>mut</code> for the same reason <code>enemy_health</code> is. Reassign it inside the loop the way we reassign health, with the old value plus <code>1</code>.</p>\n</aside>\n<hr />\n<h2 id=\"notice-what-rust-made-us-say\">Notice what Rust made us say<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#notice-what-rust-made-us-say\" aria-label=\"Link to this section\">#</a></h2>\n<p>We encountered three small pieces of Rust syntax in this article:</p>\n<pre><code class=\"language-text\">mut    → health needed to change\nwhile  → combat needed to repeat\n!      → we needed to express \"not defeated\"\n</code></pre>\n<p>But we did not add them because they appeared next on a Rust checklist.</p>\n<p>The game gave us reasons to need them.</p>\n<p>Enemy health needed to change:</p>\n<pre><code class=\"language-rust\">let mut enemy_health = 20;\n</code></pre>\n<p>Combat needed to repeat until the enemy is defeated:</p>\n<pre><code class=\"language-rust\">while !is_defeated(enemy_health) {\n</code></pre>\n<p>That is how we are going to keep learning Rust throughout this project.</p>\n<p>When the game creates a problem, we will learn the Rust needed to solve it.</p>\n<hr />\n<h2 id=\"testing-the-larger-rule\">Testing the larger rule<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#testing-the-larger-rule\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our existing tests still protect the smaller rules:</p>\n<pre><code class=\"language-rust\">#[test]\nfn click_damage_adds_base_and_bonus() {\n    assert_eq!(click_damage(3, 4), 7);\n}\n\n#[test]\nfn health_never_drops_below_zero() {\n    assert_eq!(health_after_hit(3, 10), 0);\n}\n\n#[test]\nfn zero_health_is_defeated() {\n    assert!(is_defeated(0));\n    assert!(!is_defeated(1));\n}\n</code></pre>\n<p>Now we also have:</p>\n<pre><code class=\"language-rust\">#[test]\nfn attack_reduces_enemy_health() {\n    assert_eq!(attack(10, 5, 2), 3);\n}\n</code></pre>\n<p>This test is slightly different.</p>\n<p><code>attack()</code> is not doing one tiny calculation by itself.</p>\n<p>It relies on other functions.</p>\n<p>That means we are beginning to test not only individual rules, but also the larger behavior created when those rules work together.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">fn click_damage(\n    base: u32,\n    bonus: u32,\n) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn attack(\n    health: u32,\n    base_damage: u32,\n    bonus_damage: u32,\n) -&gt; u32 {\n    let damage =\n        click_damage(base_damage, bonus_damage);\n\n    health_after_hit(health, damage)\n}\n\nfn main() {\n    let mut enemy_health = 20;\n\n    while !is_defeated(enemy_health) {\n        enemy_health =\n            attack(enemy_health, 5, 2);\n\n        println!(\n            \"Enemy health: {enemy_health}\"\n        );\n    }\n\n    println!(\"Enemy defeated!\");\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(\n            health_after_hit(3, 10),\n            0\n        );\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n\n    #[test]\n    fn attack_reduces_enemy_health() {\n        assert_eq!(attack(10, 5, 2), 3);\n    }\n}\n</code></pre>\n<p>Run both:</p>\n<pre><code class=\"language-bash\">cargo run\ncargo test\n</code></pre>\n<p>Our game now fights until the enemy is defeated, and all four functions we have written are covered by tests.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we encountered:</p>\n<ul>\n<li>functions calling other functions</li>\n<li>local variables inside functions</li>\n<li><code>mut</code></li>\n<li>reassignment</li>\n<li><code>while</code></li>\n<li><code>!</code></li>\n<li>tests for composed behavior</li>\n</ul>\n<p>You do not need to memorize all of that yet.</p>\n<p>The important change is what happened to our game.</p>\n<p>We started with three independent rules:</p>\n<pre><code class=\"language-text\">damage\nhealth\ndefeat\n</code></pre>\n<p>We combined them into:</p>\n<pre><code class=\"language-text\">attack\n</code></pre>\n<p>Then repeated that attack until we had:</p>\n<pre><code class=\"language-text\">combat\n</code></pre>\n<p>Our program became more capable without any individual function becoming particularly complicated.</p>\n<hr />\n<h2 id=\"there-is-another-problem-hiding-in-our-code\">There is another problem hiding in our code<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#there-is-another-problem-hiding-in-our-code\" aria-label=\"Link to this section\">#</a></h2>\n<p>Look at this:</p>\n<pre><code class=\"language-rust\">attack(enemy_health, 5, 2)\n</code></pre>\n<p>We know what those values mean because we just wrote them.</p>\n<p><code>enemy_health</code> belongs to the enemy.</p>\n<p><code>5</code> is the player's base damage.</p>\n<p><code>2</code> is the player's bonus damage.</p>\n<p>Rust does not know any of that.</p>\n<p>As far as Rust is concerned, they are just three <code>u32</code> values being passed into a function.</p>\n<p>And our game is about to need more information:</p>\n<pre><code class=\"language-text\">player level\nplayer gold\nenemy name\nenemy maximum health\nenemy rewards\n</code></pre>\n<p>Passing more and more unrelated-looking numbers around is not going to age well.</p>\n<p>The next problem is no longer about making our functions stronger.</p>\n<p>It is about giving our data some shape.</p>\n<p>Next, we will turn the player and enemy into real Rust types of their own.</p>\n<p><strong>Small function. Bigger game.</strong></p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "One Attack Is Not a Fight.\nCanonical: https://mnixblog.netlify.app/build-a-combat-loop-in-rust/\nBuild a Combat Loop From Small Rust Functions\nOur game can now calculate damage, reduce health, and decide whether an enemy has been defeated.\nThere is a problem, though.\nThose rules do not actually attack anything yet.\nIn this article, we are going to combine our small Rust functions into a larger attack() function, let enemy health change during combat, and repeat attacks until the enemy is defeated.\nBy the end, we will have our first actual game mechanic: a combat loop.\nOur starting point\nYou only need three small functions to follow along:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nEach one has a single job.\nclick_damage() calculates how much damage the player deals.\nhealth_after_hit() applies that damage to an enemy's health.\nis_defeated() tells us whether the enemy has reached zero health.\nRight now, however, something else has to coordinate all three.\nThat something is main().\nmain() knows too much\nWe could attack an enemy like this:\nfn main() {\nlet enemy_health = 10;\nlet damage = click_damage(5, 2);\nlet remaining_health =\nhealth_after_hit(enemy_health, damage);\nprintln!(\"Enemy health: {remaining_health}\");\nif is_defeated(remaining_health) {\nprintln!(\"Enemy defeated!\");\n}\n}\nTechnically, this does work.\nBut look at everything that main() knows about.\nIt knows how damage is calculated.\nIt knows how damage affects health.\nIt knows how to determine whether the enemy has been defeated.\nThose first two steps describe one larger idea:\nan attack.\nSo let us give that idea a function.\nBuild an attack from smaller functions\nAdd this beneath the functions we already have:\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nNotice how we are not adding new Rust ideas here.\nThat is the point.\nWe are taking functions we already understand and using them to build something more complex.\nThe first line inside attack():\nlet damage =\nclick_damage(base_damage, bonus_damage);\nuses our existing damage rule.\nThen:\nhealth_after_hit(health, damage)\nuses the result to calculate the enemy's remaining health.\nBecause it is the final expression and has no semicolon, that remaining health becomes the value returned by attack().\nOur functions now fit together like this:\nclick_damage() ───────┐\n├──→ attack()\nhealth_after_hit() ───┘\nWe did not replace our small functions.\nWe composed a larger behavior from them.\nInstead of teaching attack() every rule from scratch, it delegates those jobs to functions that already know how to calculate damage and reduce health.\nWe already know what each piece does, so understanding attack() does not require us to understand an entirely new system.\nTry it: this runs attack() against an enemy with ten health. It should print Enemy health: 3. Change the bonus damage from 2 to 1000 and predict the result before you run it.\nLive example · try changing the bonus damage\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet remaining_health = attack(10, 5, 2);\nprintln!(\"Enemy health: {remaining_health}\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/).\nQuestion: Why does attack() call click_damage() and health_after_hit() instead of repeating their arithmetic?\nRust does not allow arithmetic inside a function that returns u32\nThe damage and health rules stay in one place each, so a change there changes every attack\nCalling a function is always faster than writing the arithmetic inline\nAnswer: 2. The damage and health rules stay in one place each, so a change there changes every attack\nExplanation: Composition reuses the rules we already trust. Changing click_damage() later changes every attack without touching attack().\nTry the attack\nReplace main() with:\nfn main() {\nlet enemy_health = 10;\nlet remaining_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {remaining_health}\");\n}\nRun the game:\ncargo run\nYou should see:\nEnemy health: 3\nOur player dealt seven damage to an enemy with ten health.\nThat means attack() works.\nWe can test it too.\nAdd this to the test module:\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\nThen run:\ncargo test\nWe now have a test for behavior that depends on several smaller rules working together.\nOne attack is not much of a fight\nWe can attack an enemy once.\nAn idle game probably needs a little more combat than that.\nSuppose our enemy starts with twenty health:\nlet enemy_health = 20;\nThe first attack reduces it to thirteen.\nThe next should reduce it to six.\nThe next should reduce it to zero.\nThere is a problem, though.\nThis:\nlet enemy_health = 20;\ncreates a variable that Rust does not allow us to change.\nVariables are immutable by default in Rust.\nThat means this is not allowed:\nlet enemy_health = 20;\nenemy_health = attack(enemy_health, 5, 2);\nIf you try this, Rust will refuse to compile it and tell us that we cannot assign to an immutable variable.\nThat error is useful. We have to explicitly tell Rust which values we expect to change.\nFor most values, that is a useful default.\nFor health during combat, it is not.\nHealth is supposed to change.\nLet health change with mut\nRust uses the mut keyword when we want a variable to be mutable.\nChange:\nlet enemy_health = 20;\nto:\nlet mut enemy_health = 20;\nNow we can assign a new value to it:\nenemy_health =\nattack(enemy_health, 5, 2);\nThe word mut is short for mutable, meaning the value bound to this variable may change.\nWe are not making everything mutable just because we can.\nWe are using mutability because enemy health represents something that changes as part of the game.\nThat distinction will become increasingly useful as our state grows.\nQuestion: Why does the combat loop declare let mut enemy_health = 20;?\nwhile loops only work with variables declared mut\nprintln! needs a mutable variable to display it\nThe loop assigns a new health value after every attack\nAnswer: 3. The loop assigns a new health value after every attack\nExplanation: mut is needed because of the reassignment enemy_health = attack(...), not because of the loop or the printing.\nAttack again\nWe can now do this:\nfn main() {\nlet mut enemy_health = 20;\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\n}\nRun it:\ncargo run\nand we get:\nEnemy health: 13\nEnemy health: 6\nEnemy health: 0\nIt works.\nIt is also repetitive.\nWe know what should happen:\nKeep attacking while the enemy is still alive.\nRust has a way to express exactly that.\nRepeat with while\nA while loop repeats a block of code as long as a condition remains true.\nFor example:\nwhile enemy_health > 0 {\n// repeat something\n}\nWe could use that.\nBut we already wrote a function that knows whether the enemy has been defeated:\nis_defeated(enemy_health)\nWe should use the rule we already have.\nWhat we really want to say is:\nKeep attacking while the enemy is not defeated.\nRust uses ! to mean not.\nSo:\n!is_defeated(enemy_health)\nmeans:\nthe enemy has not been defeated\nPut that inside a while loop:\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\n}\nNow Rust keeps running the block until is_defeated() returns true.\nTry it: this is the whole loop. It should print Enemy health: 13, Enemy health: 6, Enemy health: 0, then Enemy defeated!. Change the starting health to 50 and predict how many lines of health you will see, then change it to 0 and predict again.\nLive example · try changing the starting health\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health = attack(enemy_health, 5, 2);\nprintln!(\"Enemy health: {enemy_health}\");\n}\nprintln!(\"Enemy defeated!\");\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/).\nQuestion: What does the loop above print if enemy_health starts at 0?\nEnemy health: 0 and then Enemy defeated!\nOnly Enemy defeated!\nNothing, because the program fails to compile\nAnswer: 2. Only Enemy defeated!\nExplanation: while checks its condition before each pass. !is_defeated(0) is already false, so the body never runs and only the final println! does.\nOur first combat loop\nUpdate main():\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\n\"Enemy health: {enemy_health}\"\n);\n}\nprintln!(\"Enemy defeated!\");\n}\nRun:\ncargo run\nYou should see:\nEnemy health: 13\nEnemy health: 6\nEnemy health: 0\nEnemy defeated!\nWe have a combat loop.\nThe player attacks.\nHealth changes.\nThe game checks whether combat should continue.\nThen it attacks again.\nOur little system now looks something like this:\nclick_damage() ───────┐\n├──→ attack()\nhealth_after_hit() ───┘\n↓\nrepeat while\nenemy has not been defeated\nThe individual pieces are still small.\nThe behavior is getting larger.\nCoding challenge: Count the swings\nA fight is more fun when the game can say how long it took. Add a counter that goes up once for every attack, so the program reports how many swings it took to defeat the enemy. The loop now lives in a small fight function so the count can be handed back, and Check my code will tell you how you are doing. Keep the name and signature of fight.\nExpected result: the program prints the three health lines, Enemy defeated!, and then Swings: 3. As written, the starter ends with Swings: 0.\nCount the swings\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet damage = click_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn fight(mut enemy_health: u32, base_damage: u32, bonus_damage: u32) -> u32 {\nlet mut swings = 0;\nwhile !is_defeated(enemy_health) {\nenemy_health = attack(enemy_health, base_damage, bonus_damage);\n// TODO: add 1 to swings every time the player attacks\nprintln!(\"Enemy health: {enemy_health}\");\n}\nswings\n}\nfn main() {\nlet swings = fight(20, 5, 2);\nprintln!(\"Enemy defeated!\");\nprintln!(\"Swings: {swings}\");\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// The fight should report how many attacks it took. Look at what `fight` hands back at the end.\n#[test]\nfn counts_every_swing() {\nassert_eq!(fight(20, 5, 2), 3);\n}\n/// An enemy that falls to one hit still took a swing. Check what a short fight reports.\n#[test]\nfn one_hit_is_one_swing() {\nassert_eq!(fight(7, 5, 2), 1);\n}\n/// A longer fight should report a longer count. The number has to come from the loop, not from us.\n#[test]\nfn long_fight_counts_more() {\nassert_eq!(fight(70, 5, 2), 10);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/).\nTip:\nswings is mut for the same reason enemy_health is. Reassign it inside the loop the way we reassign health, with the old value plus 1.\nNotice what Rust made us say\nWe encountered three small pieces of Rust syntax in this article:\nmut    → health needed to change\nwhile  → combat needed to repeat\n!      → we needed to express \"not defeated\"\nBut we did not add them because they appeared next on a Rust checklist.\nThe game gave us reasons to need them.\nEnemy health needed to change:\nlet mut enemy_health = 20;\nCombat needed to repeat until the enemy is defeated:\nwhile !is_defeated(enemy_health) {\nThat is how we are going to keep learning Rust throughout this project.\nWhen the game creates a problem, we will learn the Rust needed to solve it.\nTesting the larger rule\nOur existing tests still protect the smaller rules:\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\nNow we also have:\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\nThis test is slightly different.\nattack() is not doing one tiny calculation by itself.\nIt relies on other functions.\nThat means we are beginning to test not only individual rules, but also the larger behavior created when those rules work together.\nThe complete program\nAt this point, src/main.rs should look like this:\nfn click_damage(\nbase: u32,\nbonus: u32,\n) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn attack(\nhealth: u32,\nbase_damage: u32,\nbonus_damage: u32,\n) -> u32 {\nlet damage =\nclick_damage(base_damage, bonus_damage);\nhealth_after_hit(health, damage)\n}\nfn main() {\nlet mut enemy_health = 20;\nwhile !is_defeated(enemy_health) {\nenemy_health =\nattack(enemy_health, 5, 2);\nprintln!(\n\"Enemy health: {enemy_health}\"\n);\n}\nprintln!(\"Enemy defeated!\");\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(\nhealth_after_hit(3, 10),\n0\n);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n#[test]\nfn attack_reduces_enemy_health() {\nassert_eq!(attack(10, 5, 2), 3);\n}\n}\nRun both:\ncargo run\ncargo test\nOur game now fights until the enemy is defeated, and all four functions we have written are covered by tests.\nWhat we learned\nAlong the way, we encountered:\nfunctions calling other functions\nlocal variables inside functions\nmut\nreassignment\nwhile\n!\ntests for composed behavior\nYou do not need to memorize all of that yet.\nThe important change is what happened to our game.\nWe started with three independent rules:\ndamage\nhealth\ndefeat\nWe combined them into:\nattack\nThen repeated that attack until we had:\ncombat\nOur program became more capable without any individual function becoming particularly complicated.\nThere is another problem hiding in our code\nLook at this:\nattack(enemy_health, 5, 2)\nWe know what those values mean because we just wrote them.\nenemy_health belongs to the enemy.\n5 is the player's base damage.\n2 is the player's bonus damage.\nRust does not know any of that.\nAs far as Rust is concerned, they are just three u32 values being passed into a function.\nAnd our game is about to need more information:\nplayer level\nplayer gold\nenemy name\nenemy maximum health\nenemy rewards\nPassing more and more unrelated-looking numbers around is not going to age well.\nThe next problem is no longer about making our functions stronger.\nIt is about giving our data some shape.\nNext, we will turn the player and enemy into real Rust types of their own.\nSmall function. Bigger game.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/build-a-combat-loop-in-rust/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-11T23:14:06Z",
      "id": "https://mnixblog.netlify.app/build-a-combat-loop-in-rust/",
      "image": "https://mnixblog.netlify.app/images/build-a-combat-loop-in-rust/cover.jpg",
      "summary": "Learn Rust by turning small functions into a combat loop. Build an attack() function, use mut and while, and create your first real game mechanic.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "One Attack Is Not a Fight.",
      "url": "https://mnixblog.netlify.app/build-a-combat-loop-in-rust/"
    },
    {
      "content_html": "<p>Are you curious about diving into Rust, but are tired of tutorials that stop at <code>Hello World!</code>?</p>\n<p>In this article, I am going to walk you through writing and testing three tiny Rust functions that do something slightly more interesting: calculate attack damage, reduce an enemy's health, and decide whether the enemy has been defeated.</p>\n<p>You do not need any previous Rust experience to follow along.</p>\n<p>If you are able to edit a file and run a command in your terminal, you can jump right in.</p>\n<p>By the end, we will have the first real rules of an idle game, and these functions will not be throwaway tutorial code. We will keep building with them until they are running inside a complete browser game powered by Rust compiled to WebAssembly.</p>\n<p>If you are getting excited, then without further ado, it is time to start dealing damage!</p>\n<hr />\n<h2 id=\"create-the-project\">Create the project<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#create-the-project\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our first step is ensuring that Rust is installed on our local machine.</p>\n<blockquote>\n<p><strong>Need Rust installed first?</strong><br />\nRust is installed and managed through <code>rustup</code>, the official Rust toolchain installer. Follow the <a href=\"https://rust-lang.org/install.html\">official Rust installation instructions</a> for your operating system, then open a new terminal when the installation finishes.</p>\n</blockquote>\n<p>You can check that everything is ready with:</p>\n<pre><code class=\"language-bash\">rustc --version\ncargo --version\n</code></pre>\n<p>If both commands print version numbers, we are all set.</p>\n<p>We can create a new project by typing:</p>\n<pre><code class=\"language-bash\">cargo new rustbound-idle\ncd rustbound-idle\n</code></pre>\n<p>Cargo will create a small Rust application for us:</p>\n<pre><code class=\"language-text\">rustbound-idle/\n├── Cargo.toml\n└── src/\n    └── main.rs\n</code></pre>\n<p>Open <code>src/main.rs</code>.</p>\n<p>Notice the traditional Rust greeting:</p>\n<pre><code class=\"language-rust\">fn main() {\n    println!(\"Hello, world!\");\n}\n</code></pre>\n<p>If you want, you can run it once:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>Then chuck it in the bin.</p>\n<p>We are not here to say hello to enemies.</p>\n<p>We are here to say goodbye to them.</p>\n<hr />\n<h2 id=\"our-first-game-rule\">Our first game rule<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#our-first-game-rule\" aria-label=\"Link to this section\">#</a></h2>\n<p>Imagine our player has two sources of attack power:</p>\n<ul>\n<li>\n<p>base damage</p>\n</li>\n<li>\n<p>bonus damage from upgrades</p>\n</li>\n</ul>\n<p>Calculating total click damage is just addition.</p>\n<p>Add this to <code>main.rs</code>:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>This is our first Rust function.</p>\n<p>Breaking it apart into bits and pieces:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32\n</code></pre>\n<p><code>fn</code> means we are defining a function.</p>\n<p>The function name is:</p>\n<pre><code class=\"language-text\">click_damage\n</code></pre>\n<p>It takes two values:</p>\n<pre><code class=\"language-rust\">base: u32\nbonus: u32\n</code></pre>\n<p><code>u32</code> is the type of both values. A type tells Rust what kind of data a value represents and what operations make sense for it.</p>\n<p>The colon connects the parameter name to its type, so:</p>\n<pre><code class=\"language-rust\">base: u32\n</code></pre>\n<p>means that <code>base</code> is a value with a type of <code>u32</code>.</p>\n<p>For now, think of <code>u32</code> as a non-negative whole number. More specifically, it is an unsigned 32-bit integer.</p>\n<p>So these are valid <code>u32</code> values:</p>\n<pre><code class=\"language-text\">0\n1\n25\n1000\n</code></pre>\n<p>But this is not:</p>\n<pre><code class=\"language-text\">-5\n</code></pre>\n<p>After the parameters, we have:</p>\n<pre><code class=\"language-rust\">-&gt; u32\n</code></pre>\n<p>This tells Rust that the function returns a value whose type is also <code>u32</code>.</p>\n<p>Then:</p>\n<pre><code class=\"language-rust\">{\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>is the value we return.</p>\n<p>Rust lets the last expression in a function become its return value by leaving off the semicolon.</p>\n<p>So:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>means:</p>\n<blockquote>\n<p>Take the player's base damage and bonus damage, add them together, and return the result.</p>\n</blockquote>\n<p>Quick check before we move on:</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong><code>click_damage</code> has no <code>return</code> keyword. Why does it still hand back a <code>u32</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Its last line is an expression with no semicolon, so that value becomes the result</li>\n<li>Rust returns the first parameter when a function has no <code>return</code></li>\n<li><code>saturating_add</code> always sends its result back to <code>main()</code> on its own</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. Its last line is an expression with no semicolon, so that value becomes the result</p>\n<p>Leaving off the semicolon makes <code>base.saturating_add(bonus)</code> the function's final expression, and the final expression is what the function returns.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"why-not-just-use-base-bonus\">Why not just use <code>base + bonus</code>?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#why-not-just-use-base-bonus\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could. For most ordinary values, both versions give us the same answer. However, <code>u32</code> has a maximum value.</p>\n<p><code>saturating_add()</code> gives our game explicit behavior at that boundary: damage stops at the largest value <code>u32</code> can represent instead of leaving overflow behavior to the build configuration.</p>\n<p>This is a <strong>game-design choice</strong>, not a rule that Rust programs should always use saturating arithmetic.</p>\n<p>We are choosing it because damage is a quantity where hitting the numeric ceiling makes more sense than allowing overflow.</p>\n<p>Try it: run this, then push the ceiling yourself. It should print <code>7</code>, then <code>4294967295</code>, which is the largest <code>u32</code>. Change the second call to <code>click_damage(u32::MAX, 1000)</code> and predict the result before you run it.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try pushing past the ceiling</figcaption><pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn main() {\n    println!(&quot;{}&quot;, click_damage(5, 2));\n    println!(&quot;{}&quot;, click_damage(u32::MAX, 1));\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/skip-hello-world/\">the original article</a>.</p></figure>\n<p>Now swap <code>base.saturating_add(bonus)</code> for <code>base + bonus</code> and run it again. The Playground builds in debug mode, so the second line stops with an overflow panic instead of printing a number.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>In this game, what does <code>saturating_add</code> do when the true total is larger than the biggest <code>u32</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>It stops at the largest <code>u32</code> value</li>\n<li>It wraps around and starts again from <code>0</code></li>\n<li>It turns the result into a negative number</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: A. It stops at the largest <code>u32</code> value</p>\n<p>\"Saturating\" means the value sticks at the ceiling. A <code>u32</code> can never be negative, and <code>saturating_add</code> never wraps.</p>\n</div>\n</div>\n</section>\n<hr />\n<h2 id=\"call-the-function\">Call the function<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#call-the-function\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now we can give <code>main()</code> something to do:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn main() {\n    let damage = click_damage(5, 2);\n\n    println!(\"You dealt {damage} damage!\");\n}\n</code></pre>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">You dealt 7 damage!\n</code></pre>\n<p>Technically, we did not escape <code>println!()</code> after all.</p>\n<p>But at least we are using it to deal some damage as promised.</p>\n<hr />\n<h2 id=\"give-the-enemy-some-health\">Give the enemy some health<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#give-the-enemy-some-health\" aria-label=\"Link to this section\">#</a></h2>\n<p>Damage is not useful if our enemies do not have a health pool.</p>\n<p>Our second rule will calculate how much health remains after an attack:</p>\n<pre><code class=\"language-rust\">fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n</code></pre>\n<p>Notice how this looks a lot like our first function.</p>\n<p>It takes:</p>\n<pre><code class=\"language-rust\">health\ndamage\n</code></pre>\n<p>and returns the remaining health.</p>\n<p>Again, we are using saturating arithmetic.</p>\n<p>Why?</p>\n<p>Imagine the enemy has:</p>\n<pre><code class=\"language-text\">3 health\n</code></pre>\n<p>and we deal:</p>\n<pre><code class=\"language-text\">10 damage\n</code></pre>\n<p>For our game, we want the result to be:</p>\n<pre><code class=\"language-text\">0\n</code></pre>\n<p>not:</p>\n<pre><code class=\"language-text\">-7\n</code></pre>\n<p>Health cannot drop below zero.</p>\n<p>So:</p>\n<pre><code class=\"language-rust\">health.saturating_sub(damage)\n</code></pre>\n<p>expresses this game rule clearly.</p>\n<p>Try it: this program should print <code>0</code> twice, once for an enemy with 3 health taking 10 damage and once for exactly 3 damage. Change the first call to <code>health_after_hit(30, 10)</code> and predict the new first line.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Live example · try changing the damage</figcaption><pre><code class=\"language-rust\">fn health_after_hit(health: u32, damage: u32) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn main() {\n    println!(&quot;{}&quot;, health_after_hit(3, 10));\n    println!(&quot;{}&quot;, health_after_hit(3, 3));\n}\n</code></pre><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/skip-hello-world/\">the original article</a>.</p></figure>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Why does <code>health_after_hit(3, 10)</code> give <code>0</code> instead of <code>-7</code>?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Rust rounds negative answers up to zero for every subtraction</li>\n<li>A <code>u32</code> cannot be negative, and <code>saturating_sub</code> stops at <code>0</code></li>\n<li>The call fails to compile because <code>10</code> is bigger than <code>3</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. A <code>u32</code> cannot be negative, and <code>saturating_sub</code> stops at <code>0</code></p>\n<p>The rule is a deliberate choice of <code>saturating_sub</code>. Plain subtraction on a <code>u32</code> does not clamp; it would overflow instead.</p>\n</div>\n</div>\n</section>\n<p>Try it:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let damage = click_damage(5, 2);\n    let remaining_health = health_after_hit(10, damage);\n\n    println!(\"You dealt {damage} damage!\");\n    println!(\"Enemy health: {remaining_health}\");\n}\n</code></pre>\n<p>Run again:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>You should see:</p>\n<pre><code class=\"language-text\">You dealt 7 damage!\nEnemy health: 3\n</code></pre>\n<p>Now we are starting to have something that resembles combat.</p>\n<p>Barely. But it counts.</p>\n<hr />\n<h2 id=\"is-the-enemy-defeated\">Is the enemy defeated?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#is-the-enemy-defeated\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our third rule is even smaller:</p>\n<pre><code class=\"language-rust\">fn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n</code></pre>\n<p>This introduces another Rust type:</p>\n<pre><code class=\"language-text\">bool\n</code></pre>\n<p>A boolean has two possible values:</p>\n<pre><code class=\"language-text\">true\nfalse\n</code></pre>\n<p>Our function answers one question:</p>\n<blockquote>\n<p>Has the enemy's health reached zero?</p>\n</blockquote>\n<p>If we call:</p>\n<pre><code class=\"language-rust\">is_defeated(0)\n</code></pre>\n<p>we get:</p>\n<pre><code class=\"language-text\">true\n</code></pre>\n<p>If we call:</p>\n<pre><code class=\"language-rust\">is_defeated(10)\n</code></pre>\n<p>we get:</p>\n<pre><code class=\"language-text\">false\n</code></pre>\n<p>It is time to use it.</p>\n<pre><code class=\"language-rust\">fn main() {\n    let enemy_health = 5;\n\n    let damage = click_damage(5, 2);\n    let remaining_health =\n        health_after_hit(enemy_health, damage);\n\n    println!(\"You dealt {damage} damage!\");\n    println!(\"Enemy health: {remaining_health}\");\n\n    if is_defeated(remaining_health) {\n        println!(\"Enemy defeated!\");\n    }\n}\n</code></pre>\n<p>Run it:</p>\n<pre><code class=\"language-bash\">cargo run\n</code></pre>\n<p>and now we get:</p>\n<pre><code class=\"language-text\">You dealt 7 damage!\nEnemy health: 0\nEnemy defeated!\n</code></pre>\n<p>Now we have combat rules!</p>\n<p>Tiny ones. But real ones.</p>\n<p><strong>Coding challenge: Armor</strong></p>\n<p>Enemies in an idle game eventually wear armor. Finish <code>damage_after_armor</code> so it subtracts the armor from the damage and never goes below zero, just like <code>health_after_hit</code> does for health. Keep the name and signature of <code>damage_after_armor</code>, and press <strong>Check my code</strong> to see how you are doing.</p>\n<p><strong>Expected result:</strong> the program prints <code>4</code> and then <code>0</code>. As written, the starter prints <code>7</code> and then <code>2</code>.</p>\n<figure class=\"ob-code-block ob-runnable-code\"><figcaption class=\"ob-code-title\">Finish the armor rule</figcaption><pre><code class=\"language-rust\">fn damage_after_armor(damage: u32, armor: u32) -&gt; u32 {\n    // TODO: subtract the armor from the damage, but never go below 0\n    damage\n}\n\nfn main() {\n    println!(&quot;{}&quot;, damage_after_armor(7, 3));\n    println!(&quot;{}&quot;, damage_after_armor(2, 5));\n}\n\n</code></pre><details class=\"ob-spoiler\"><summary>Spoiler: the checks for this challenge</summary><p>These tests run when you press Check my code on the original article. Reading them first gives away the answer.</p><pre><code class=\"language-rust\">/// The armor should come off the damage. Look at what the function hands back.\n#[test]\nfn armor_reduces_damage() {\n    assert_eq!(damage_after_armor(10, 4), 6);\n}\n\n/// A weak hit against heavy armor is still only a weak hit. Look at what happens when the armor is the bigger number.\n#[test]\nfn never_goes_below_zero() {\n    assert_eq!(damage_after_armor(2, 5), 0);\n}\n\n/// Armor that matches the damage exactly is the edge. Check which side of it your rule lands on.\n#[test]\nfn armor_equal_to_damage() {\n    assert_eq!(damage_after_armor(6, 6), 0);\n}\n</code></pre></details><p class=\"ob-portable-note\">Run and edit this example on <a href=\"https://mnixblog.netlify.app/skip-hello-world/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>This is the same shape as <code>health_after_hit</code>. Which method did we use there to subtract without going below zero?</p>\n</aside>\n<hr />\n<h2 id=\"why-make-these-separate-functions\">Why make these separate functions?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#why-make-these-separate-functions\" aria-label=\"Link to this section\">#</a></h2>\n<p>We could have written everything directly inside <code>main()</code>:</p>\n<pre><code class=\"language-rust\">fn main() {\n    let health = 5;\n    let base_damage = 5;\n    let bonus_damage = 2;\n\n    let damage = base_damage + bonus_damage;\n    let remaining_health =\n        health.saturating_sub(damage);\n\n    if remaining_health == 0 {\n        println!(\"Enemy defeated!\");\n    }\n}\n</code></pre>\n<p>That works. Especially for a program this small, and it may even look easier.</p>\n<p>But this game is not going to stay this small.</p>\n<p>Eventually we will need to answer questions like:</p>\n<ul>\n<li>How much damage does the player deal?</li>\n<li>Did the enemy die?</li>\n<li>How much gold should the player receive?</li>\n<li>How much XP did they earn?</li>\n<li>Did they level up?</li>\n<li>How strong should the next enemy be?</li>\n<li>How much passive damage happened while the player was away?</li>\n</ul>\n<p>If all of those rules live inside one giant function, the program gets harder to understand very quickly.</p>\n<p>Instead, we are starting with small pieces that have clear jobs:</p>\n<pre><code class=\"language-text\">click_damage()\nhealth_after_hit()\nis_defeated()\n</code></pre>\n<p>Later, bigger functions will use these functions.</p>\n<p>Then even bigger behavior will use those.</p>\n<p>That is how we are going to build the entire game.</p>\n<p>Not by writing one giant clever function.</p>\n<p>By combining small, boring ones until the result is no longer boring.</p>\n<hr />\n<h2 id=\"testing-the-rules\">Testing the rules<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#testing-the-rules\" aria-label=\"Link to this section\">#</a></h2>\n<p>These functions have another useful property:</p>\n<p>They are easy to test.</p>\n<p>We give them input.</p>\n<p>We check the output.</p>\n<p>At the bottom of <code>main.rs</code>, add:</p>\n<pre><code class=\"language-rust\">#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n}\n</code></pre>\n<p>Run:</p>\n<pre><code class=\"language-bash\">cargo test\n</code></pre>\n<p>You should see three passing tests.</p>\n<p>We are not going to turn this into a giant testing lesson yet.</p>\n<p>For now, notice how easy it is to verify a small function with a clear input and output.</p>\n<p>That will matter more as the game grows.</p>\n<hr />\n<h2 id=\"the-complete-program\">The complete program<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#the-complete-program\" aria-label=\"Link to this section\">#</a></h2>\n<p>At this point, <code>src/main.rs</code> should look like this:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n\nfn health_after_hit(\n    health: u32,\n    damage: u32,\n) -&gt; u32 {\n    health.saturating_sub(damage)\n}\n\nfn is_defeated(health: u32) -&gt; bool {\n    health == 0\n}\n\nfn main() {\n    let enemy_health = 5;\n\n    let damage = click_damage(5, 2);\n\n    let remaining_health =\n        health_after_hit(enemy_health, damage);\n\n    println!(\"You dealt {damage} damage!\");\n    println!(\"Enemy health: {remaining_health}\");\n\n    if is_defeated(remaining_health) {\n        println!(\"Enemy defeated!\");\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn click_damage_adds_base_and_bonus() {\n        assert_eq!(click_damage(3, 4), 7);\n    }\n\n    #[test]\n    fn health_never_drops_below_zero() {\n        assert_eq!(health_after_hit(3, 10), 0);\n    }\n\n    #[test]\n    fn zero_health_is_defeated() {\n        assert!(is_defeated(0));\n        assert!(!is_defeated(1));\n    }\n}\n</code></pre>\n<p>This is not much of a game yet.</p>\n<p>And that is okay.</p>\n<p>We now have something more useful than a big pile of code, and we have three rules that we understand.</p>\n<hr />\n<h2 id=\"what-we-learned\">What we learned<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#what-we-learned\" aria-label=\"Link to this section\">#</a></h2>\n<p>Along the way, we have already encountered:</p>\n<ul>\n<li>\n<p><code>fn</code></p>\n</li>\n<li>\n<p>function parameters</p>\n</li>\n<li>\n<p>return types</p>\n</li>\n<li>\n<p><code>u32</code></p>\n</li>\n<li>\n<p><code>bool</code></p>\n</li>\n<li>\n<p><code>let</code></p>\n</li>\n<li>\n<p>expressions</p>\n</li>\n<li>\n<p><code>if</code></p>\n</li>\n<li>\n<p>function calls</p>\n</li>\n<li>\n<p>Cargo</p>\n</li>\n<li>\n<p>basic Rust tests</p>\n</li>\n</ul>\n<p>You do not need to memorize all of that yet. More importantly, we can now describe combat with three small rules:</p>\n<pre><code class=\"language-text\">calculate damage\n       ↓\nreduce health\n       ↓\ncheck for defeat\n</code></pre>\n<p>That is our first little game system.</p>\n<p>And we are keeping it.</p>\n<hr />\n<h2 id=\"where-we-will-take-it-next\">Where we will take it next<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/skip-hello-world/#where-we-will-take-it-next\" aria-label=\"Link to this section\">#</a></h2>\n<p>Right now <code>main()</code> is doing everything — calculating damage, applying it, checking for defeat. Those three steps together represent a single larger idea: an attack.</p>\n<p>Next, we will build that concept as its own function, using the three small functions we already wrote. That is where this project starts to show its real shape.</p>\n<p>Eventually a click in the browser will travel through a Web Component, into WebAssembly, and down into Rust — all the way to:</p>\n<pre><code class=\"language-rust\">fn click_damage(base: u32, bonus: u32) -&gt; u32 {\n    base.saturating_add(bonus)\n}\n</code></pre>\n<p>Small function. Bigger game.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/skip-hello-world/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Skip Hello World.\nCanonical: https://mnixblog.netlify.app/skip-hello-world/\nWrite the First Rules of an Idle Game\nAre you curious about diving into Rust, but are tired of tutorials that stop at Hello World!?\nIn this article, I am going to walk you through writing and testing three tiny Rust functions that do something slightly more interesting: calculate attack damage, reduce an enemy's health, and decide whether the enemy has been defeated.\nYou do not need any previous Rust experience to follow along.\nIf you are able to edit a file and run a command in your terminal, you can jump right in.\nBy the end, we will have the first real rules of an idle game, and these functions will not be throwaway tutorial code. We will keep building with them until they are running inside a complete browser game powered by Rust compiled to WebAssembly.\nIf you are getting excited, then without further ado, it is time to start dealing damage!\nCreate the project\nOur first step is ensuring that Rust is installed on our local machine.\nNeed Rust installed first?\nRust is installed and managed through rustup, the official Rust toolchain installer. Follow the official Rust installation instructions (https://rust-lang.org/install.html) for your operating system, then open a new terminal when the installation finishes.\nYou can check that everything is ready with:\nrustc --version\ncargo --version\nIf both commands print version numbers, we are all set.\nWe can create a new project by typing:\ncargo new rustbound-idle\ncd rustbound-idle\nCargo will create a small Rust application for us:\nrustbound-idle/\n├── Cargo.toml\n└── src/\n└── main.rs\nOpen src/main.rs.\nNotice the traditional Rust greeting:\nfn main() {\nprintln!(\"Hello, world!\");\n}\nIf you want, you can run it once:\ncargo run\nThen chuck it in the bin.\nWe are not here to say hello to enemies.\nWe are here to say goodbye to them.\nOur first game rule\nImagine our player has two sources of attack power:\nbase damage\nbonus damage from upgrades\nCalculating total click damage is just addition.\nAdd this to main.rs:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nThis is our first Rust function.\nBreaking it apart into bits and pieces:\nfn click_damage(base: u32, bonus: u32) -> u32\nfn means we are defining a function.\nThe function name is:\nclick_damage\nIt takes two values:\nbase: u32\nbonus: u32\nu32 is the type of both values. A type tells Rust what kind of data a value represents and what operations make sense for it.\nThe colon connects the parameter name to its type, so:\nbase: u32\nmeans that base is a value with a type of u32.\nFor now, think of u32 as a non-negative whole number. More specifically, it is an unsigned 32-bit integer.\nSo these are valid u32 values:\n0\n1\n25\n1000\nBut this is not:\n-5\nAfter the parameters, we have:\n-> u32\nThis tells Rust that the function returns a value whose type is also u32.\nThen:\n{\nbase.saturating_add(bonus)\n}\nis the value we return.\nRust lets the last expression in a function become its return value by leaving off the semicolon.\nSo:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nmeans:\nTake the player's base damage and bonus damage, add them together, and return the result.\nQuick check before we move on:\nQuestion: click_damage has no return keyword. Why does it still hand back a u32?\nIts last line is an expression with no semicolon, so that value becomes the result\nRust returns the first parameter when a function has no return\nsaturating_add always sends its result back to main() on its own\nAnswer: 1. Its last line is an expression with no semicolon, so that value becomes the result\nExplanation: Leaving off the semicolon makes base.saturating_add(bonus) the function's final expression, and the final expression is what the function returns.\nWhy not just use base + bonus?\nWe could. For most ordinary values, both versions give us the same answer. However, u32 has a maximum value.\nsaturating_add() gives our game explicit behavior at that boundary: damage stops at the largest value u32 can represent instead of leaving overflow behavior to the build configuration.\nThis is a game-design choice, not a rule that Rust programs should always use saturating arithmetic.\nWe are choosing it because damage is a quantity where hitting the numeric ceiling makes more sense than allowing overflow.\nTry it: run this, then push the ceiling yourself. It should print 7, then 4294967295, which is the largest u32. Change the second call to click_damage(u32::MAX, 1000) and predict the result before you run it.\nLive example · try pushing past the ceiling\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn main() {\nprintln!(\"{}\", click_damage(5, 2));\nprintln!(\"{}\", click_damage(u32::MAX, 1));\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/skip-hello-world/).\nNow swap base.saturating_add(bonus) for base + bonus and run it again. The Playground builds in debug mode, so the second line stops with an overflow panic instead of printing a number.\nQuestion: In this game, what does saturating_add do when the true total is larger than the biggest u32?\nIt stops at the largest u32 value\nIt wraps around and starts again from 0\nIt turns the result into a negative number\nAnswer: 1. It stops at the largest u32 value\nExplanation: \"Saturating\" means the value sticks at the ceiling. A u32 can never be negative, and saturating_add never wraps.\nCall the function\nNow we can give main() something to do:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn main() {\nlet damage = click_damage(5, 2);\nprintln!(\"You dealt {damage} damage!\");\n}\nRun:\ncargo run\nYou should see:\nYou dealt 7 damage!\nTechnically, we did not escape println!() after all.\nBut at least we are using it to deal some damage as promised.\nGive the enemy some health\nDamage is not useful if our enemies do not have a health pool.\nOur second rule will calculate how much health remains after an attack:\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nNotice how this looks a lot like our first function.\nIt takes:\nhealth\ndamage\nand returns the remaining health.\nAgain, we are using saturating arithmetic.\nWhy?\nImagine the enemy has:\n3 health\nand we deal:\n10 damage\nFor our game, we want the result to be:\n0\nnot:\n-7\nHealth cannot drop below zero.\nSo:\nhealth.saturating_sub(damage)\nexpresses this game rule clearly.\nTry it: this program should print 0 twice, once for an enemy with 3 health taking 10 damage and once for exactly 3 damage. Change the first call to health_after_hit(30, 10) and predict the new first line.\nLive example · try changing the damage\nfn health_after_hit(health: u32, damage: u32) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn main() {\nprintln!(\"{}\", health_after_hit(3, 10));\nprintln!(\"{}\", health_after_hit(3, 3));\n}\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/skip-hello-world/).\nQuestion: Why does health_after_hit(3, 10) give 0 instead of -7?\nRust rounds negative answers up to zero for every subtraction\nA u32 cannot be negative, and saturating_sub stops at 0\nThe call fails to compile because 10 is bigger than 3\nAnswer: 2. A u32 cannot be negative, and saturating_sub stops at 0\nExplanation: The rule is a deliberate choice of saturating_sub. Plain subtraction on a u32 does not clamp; it would overflow instead.\nTry it:\nfn main() {\nlet damage = click_damage(5, 2);\nlet remaining_health = health_after_hit(10, damage);\nprintln!(\"You dealt {damage} damage!\");\nprintln!(\"Enemy health: {remaining_health}\");\n}\nRun again:\ncargo run\nYou should see:\nYou dealt 7 damage!\nEnemy health: 3\nNow we are starting to have something that resembles combat.\nBarely. But it counts.\nIs the enemy defeated?\nOur third rule is even smaller:\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nThis introduces another Rust type:\nbool\nA boolean has two possible values:\ntrue\nfalse\nOur function answers one question:\nHas the enemy's health reached zero?\nIf we call:\nis_defeated(0)\nwe get:\ntrue\nIf we call:\nis_defeated(10)\nwe get:\nfalse\nIt is time to use it.\nfn main() {\nlet enemy_health = 5;\nlet damage = click_damage(5, 2);\nlet remaining_health =\nhealth_after_hit(enemy_health, damage);\nprintln!(\"You dealt {damage} damage!\");\nprintln!(\"Enemy health: {remaining_health}\");\nif is_defeated(remaining_health) {\nprintln!(\"Enemy defeated!\");\n}\n}\nRun it:\ncargo run\nand now we get:\nYou dealt 7 damage!\nEnemy health: 0\nEnemy defeated!\nNow we have combat rules!\nTiny ones. But real ones.\nCoding challenge: Armor\nEnemies in an idle game eventually wear armor. Finish damage_after_armor so it subtracts the armor from the damage and never goes below zero, just like health_after_hit does for health. Keep the name and signature of damage_after_armor, and press Check my code to see how you are doing.\nExpected result: the program prints 4 and then 0. As written, the starter prints 7 and then 2.\nFinish the armor rule\nfn damage_after_armor(damage: u32, armor: u32) -> u32 {\n// TODO: subtract the armor from the damage, but never go below 0\ndamage\n}\nfn main() {\nprintln!(\"{}\", damage_after_armor(7, 3));\nprintln!(\"{}\", damage_after_armor(2, 5));\n}\n<details>\n<summary>Spoiler: the checks for this challenge</summary>\nThese tests run when you press Check my code on the original article. Reading them first gives away the answer.\n/// The armor should come off the damage. Look at what the function hands back.\n#[test]\nfn armor_reduces_damage() {\nassert_eq!(damage_after_armor(10, 4), 6);\n}\n/// A weak hit against heavy armor is still only a weak hit. Look at what happens when the armor is the bigger number.\n#[test]\nfn never_goes_below_zero() {\nassert_eq!(damage_after_armor(2, 5), 0);\n}\n/// Armor that matches the damage exactly is the edge. Check which side of it your rule lands on.\n#[test]\nfn armor_equal_to_damage() {\nassert_eq!(damage_after_armor(6, 6), 0);\n}\n</details>\nRun and edit this example on the canonical article (https://mnixblog.netlify.app/skip-hello-world/).\nTip:\nThis is the same shape as health_after_hit. Which method did we use there to subtract without going below zero?\nWhy make these separate functions?\nWe could have written everything directly inside main():\nfn main() {\nlet health = 5;\nlet base_damage = 5;\nlet bonus_damage = 2;\nlet damage = base_damage + bonus_damage;\nlet remaining_health =\nhealth.saturating_sub(damage);\nif remaining_health == 0 {\nprintln!(\"Enemy defeated!\");\n}\n}\nThat works. Especially for a program this small, and it may even look easier.\nBut this game is not going to stay this small.\nEventually we will need to answer questions like:\nHow much damage does the player deal?\nDid the enemy die?\nHow much gold should the player receive?\nHow much XP did they earn?\nDid they level up?\nHow strong should the next enemy be?\nHow much passive damage happened while the player was away?\nIf all of those rules live inside one giant function, the program gets harder to understand very quickly.\nInstead, we are starting with small pieces that have clear jobs:\nclick_damage()\nhealth_after_hit()\nis_defeated()\nLater, bigger functions will use these functions.\nThen even bigger behavior will use those.\nThat is how we are going to build the entire game.\nNot by writing one giant clever function.\nBy combining small, boring ones until the result is no longer boring.\nTesting the rules\nThese functions have another useful property:\nThey are easy to test.\nWe give them input.\nWe check the output.\nAt the bottom of main.rs, add:\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n}\nRun:\ncargo test\nYou should see three passing tests.\nWe are not going to turn this into a giant testing lesson yet.\nFor now, notice how easy it is to verify a small function with a clear input and output.\nThat will matter more as the game grows.\nThe complete program\nAt this point, src/main.rs should look like this:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nfn health_after_hit(\nhealth: u32,\ndamage: u32,\n) -> u32 {\nhealth.saturating_sub(damage)\n}\nfn is_defeated(health: u32) -> bool {\nhealth == 0\n}\nfn main() {\nlet enemy_health = 5;\nlet damage = click_damage(5, 2);\nlet remaining_health =\nhealth_after_hit(enemy_health, damage);\nprintln!(\"You dealt {damage} damage!\");\nprintln!(\"Enemy health: {remaining_health}\");\nif is_defeated(remaining_health) {\nprintln!(\"Enemy defeated!\");\n}\n}\n#[cfg(test)]\nmod tests {\nuse super::*;\n#[test]\nfn click_damage_adds_base_and_bonus() {\nassert_eq!(click_damage(3, 4), 7);\n}\n#[test]\nfn health_never_drops_below_zero() {\nassert_eq!(health_after_hit(3, 10), 0);\n}\n#[test]\nfn zero_health_is_defeated() {\nassert!(is_defeated(0));\nassert!(!is_defeated(1));\n}\n}\nThis is not much of a game yet.\nAnd that is okay.\nWe now have something more useful than a big pile of code, and we have three rules that we understand.\nWhat we learned\nAlong the way, we have already encountered:\nfn\nfunction parameters\nreturn types\nu32\nbool\nlet\nexpressions\nif\nfunction calls\nCargo\nbasic Rust tests\nYou do not need to memorize all of that yet. More importantly, we can now describe combat with three small rules:\ncalculate damage\n↓\nreduce health\n↓\ncheck for defeat\nThat is our first little game system.\nAnd we are keeping it.\nWhere we will take it next\nRight now main() is doing everything — calculating damage, applying it, checking for defeat. Those three steps together represent a single larger idea: an attack.\nNext, we will build that concept as its own function, using the three small functions we already wrote. That is where this project starts to show its real shape.\nEventually a click in the browser will travel through a Web Component, into WebAssembly, and down into Rust — all the way to:\nfn click_damage(base: u32, bonus: u32) -> u32 {\nbase.saturating_add(bonus)\n}\nSmall function. Bigger game.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The source code and canonical run/edit link are preserved; eligible Rust examples also link to the external Playground. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/skip-hello-world/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2026-10-08T22:03:28Z",
      "id": "https://mnixblog.netlify.app/skip-hello-world/",
      "image": "https://mnixblog.netlify.app/images/skip-hello-world/cover.jpg",
      "summary": "Learn Rust by building real game logic instead of stopping at Hello World. Write and test three small functions for damage, health, and enemy defeat.",
      "tags": [
        "Rust",
        "WebAssembly",
        "Game Development",
        "Beginners"
      ],
      "title": "Skip Hello World.",
      "url": "https://mnixblog.netlify.app/skip-hello-world/"
    },
    {
      "content_html": "<hr />\n<p>The last time I posted was on April 28, 2025. I know the exact date because it was the same day I got married.</p>\n<p>Looking back, that might explain why this blog settled down soon after.</p>\n<p>Not because I stopped programming. I was still building, learning, and working through plenty of code. I just quit writing <em>about</em> it.</p>\n<p>As time passed, the blog seemed to drift further down the backlog. When I found myself with some free time, I was more likely to spend it squashing bugs, working on a project, or learning something new than writing about any of it.</p>\n<p>So before I start writing about what comes next, I should probably explain what has happened since then.</p>\n<p>Because it was a year.</p>\n<h2 id=\"first-a-tiny-detail-i-forgot-to-mention\">First, a tiny detail I forgot to mention<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#first-a-tiny-detail-i-forgot-to-mention\" aria-label=\"Link to this section\">#</a></h2>\n<p>While participating in a <a href=\"https://chingu.io\">Chingu</a> Voyage, I met my wife.</p>\n<p>Yes, <em>that</em> Chingu.</p>\n<p>The online platform that put together teams of software engineers and agile leaders to build projects suitable to their abilities, argue politely about Git branches, and every once in a while spend twenty minutes figuring out who forgot to pull before opening a PR.</p>\n<p>Before this past year, I had completed several Chingu Voyages, working as a developer and a product owner, and even as a mentor. One of those Voyages just happened to work out particularly well.</p>\n<p>I met her.</p>\n<p>As far as I am aware, almost nobody over there knows about it. It never really came up, and there never seemed to be a natural moment in a Discord channel to mention it. Plus it all escalated well after that Voyage ended.</p>\n<p>This blog has always been about my experience writing code and sharing the lessons I've learned in order to help other developers grow and tap into their potential.</p>\n<p>So this may seem like it comes out of left field, but Chingu gave me teammates, portfolio projects, experience working on software teams...</p>\n<p>...and a wife.</p>\n<p>Maybe not in that order.</p>\n<p>You might say that it was a successful Voyage.</p>\n<h2 id=\"before-the-baby-there-was-baby-book\">Before the Baby, There Was Baby Book<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#before-the-baby-there-was-baby-book\" aria-label=\"Link to this section\">#</a></h2>\n<p>Before summer could even arrive, we found out our lives were about to change again!</p>\n<p>We found out we were expecting a son, due November 7. We began to think about all of the little things that come with two people becoming parents.</p>\n<p>Naturally, we both looked at each other and said:</p>\n<blockquote>\n<p>We could probably build an app for this.</p>\n</blockquote>\n<p>So we began working on an idea we called <strong>Baby Book</strong>.</p>\n<p>In the first phase, it was only a household task and grocery list tracking app. The long-term plan was to track milestones, achievements, appointments, and eventually the little things one parent might miss while the other was there—so we could stay on the same page.</p>\n<p>More or less, we set out to build a shared record of our new favorite person's life as it began to unfold.</p>\n<p>We never did polish the application to the original design plan, but for a little while it was actually useful.</p>\n<p>When she went into labor, I threw together a quick feature for helping her with breathing and contractions. When our son was born, Baby Book shifted toward the realities of parenthood. We started tracking feeding times and details, diaper changes, and little notes.</p>\n<p>It did not take long before we did what most new parents probably do.</p>\n<p>We fell into a rhythm and stopped stressing over those details as much.</p>\n<p>Our need for this app faded, and Baby Book quietly moved into the category titled:</p>\n<blockquote>\n<p>I should really come back to this someday.</p>\n</blockquote>\n<p>And I still may.</p>\n<p>Some of the original idea could still be useful today, especially around milestones and giving us an easy way to share the little things the other parent might miss.</p>\n<p>But it is not exactly at the top of the backlog right now.</p>\n<h2 id=\"a-wild-son-has-appeared\">A Wild Son Has Appeared<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#a-wild-son-has-appeared\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our son was supposed to arrive on <strong>November 7</strong>.</p>\n<p>Apparently, he had his own release schedule. He arrived <strong>October 12</strong>, about four weeks early.</p>\n<p>Anybody who has had an infant can probably relate to what happened to my schedule next. If you haven't, imagine handing your calendar to a tiny version of yourself that only speaks babble, uses his mouth to read, and thinks 3:17 in the morning is the perfect time to discuss politics.</p>\n<p>That is roughly the system, right?</p>\n<p>The funny part is that another fairly important date had happened only five days earlier.</p>\n<p>On <strong>October 7</strong>, I was accepted into Code the Dream's React course.</p>\n<h2 id=\"code-the-dream-was-already-in-motion\">Code the Dream Was Already in Motion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#code-the-dream-was-already-in-motion\" aria-label=\"Link to this section\">#</a></h2>\n<p>I had actually applied to <a href=\"https://codethedream.org/\"><strong>Code the Dream</strong></a> months earlier, around April 2025.</p>\n<p>The timing of the whole thing could have been a little better. But it is not like I brought the baby home, learned a thing or two about our new tiny boss, and decided:</p>\n<blockquote>\n<p>You know what this situation needs?</p>\n</blockquote>\n<blockquote>\n<p>Another commitment.</p>\n</blockquote>\n<p>The decision had already been made.</p>\n<p>I had tested out of the Intro to Programming course, which is how I ended up going directly into React.</p>\n<p>By this point, I knew that it was going to be a challenge, but it seemed reasonable enough.</p>\n<p>Strangely enough, his early arrival may actually have made the start of the course easier. I lost some preparation time, but I gained a few weeks to settle into being a parent before React started. And that made a big difference.</p>\n<p>By the time the React cohort started, I was not deciding whether I wanted to begin another programming course while raising a newborn. I had already committed to the course.</p>\n<p>Now I just had to figure out what continuing looked like under completely different circumstances.</p>\n<p>The answer was very different from the way I had worked before.</p>\n<p>Long, uninterrupted coding sessions became harder to find. Development started happening in smaller windows: thirty minutes here, an hour there, whatever time happened to exist between everything else.</p>\n<p>If twenty minutes of a thirty-five-minute session were spent trying to remember what I had been doing the last time I opened the project, that was a problem.</p>\n<p>So I started leaving myself better notes. I broke problems into smaller pieces. I became more deliberate about commits and less interested in keeping an entire feature in my head.</p>\n<p>Without really planning to, becoming a parent forced me to get better at stopping halfway through something while leaving enough breadcrumbs that future me could figure out what past me had been doing.</p>\n<p>And then React started.</p>\n<h2 id=\"react-the-second-time-around\">React, the Second Time Around<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#react-the-second-time-around\" aria-label=\"Link to this section\">#</a></h2>\n<p>I joined Code the Dream's React v3 course as part of the <strong>Lark Cohort</strong>, which ran from November through February.</p>\n<p>Code the Dream was not where I first learned React, or where I first built a full-stack application. I had already built MERN applications, worked with APIs, Express, and MongoDB, and completed several development projects with other people through Chingu.</p>\n<p>What CTD gave me was something different.</p>\n<p>It gave me several months to go back through technologies I already knew and find the places where my understanding was thinner than I realized.</p>\n<p>One of those places was <strong>Context</strong>.</p>\n<p>I had used Context before and understood what it did, but there is a difference between knowing that Context exists and getting comfortable designing state around it, combining it with reducers, deciding what actually belongs there, and thinking about what happens as your component tree gets larger.</p>\n<p>Sometimes learning something for the second time is a lot more useful than learning it the first time.</p>\n<p>The first time, you are mostly trying to make it work.</p>\n<p>The second time, you start asking why it works that way.</p>\n<p>That became a recurring theme during the course. It was less:</p>\n<blockquote>\n<p>Here is React.</p>\n</blockquote>\n<p>and more:</p>\n<blockquote>\n<p>You know React. Now let's find the parts you have been getting away with not understanding deeply enough.</p>\n</blockquote>\n<p>My guided project grew throughout the course. Reducers came in, Context became more important, routing got more complicated, external data got involved, state crossed more boundaries, and testing became a larger part of the way I worked.</p>\n<p>Eventually something I had built myself six weeks earlier could surprise me, which is always reassuring.</p>\n<p>Somewhere during that process, the question I asked after completing something changed.</p>\n<p>It used to be:</p>\n<blockquote>\n<p>Does it work?</p>\n</blockquote>\n<p>I would click around a few times, see that everything looked fine, and move on.</p>\n<p>Now I am much more likely to ask:</p>\n<blockquote>\n<p>Okay, it works. What breaks it?</p>\n</blockquote>\n<p>What happens if the request fails? What happens if the user refreshes here? What if this state has not hydrated yet? What happens if the component disappears before an asynchronous operation finishes? What happens if somebody clicks the same button five times while the first request is still running?</p>\n<p>The happy path is still important.</p>\n<p>I just trust it a lot less than I used to.</p>\n<h2 id=\"then-i-went-back-through-the-backend\">Then I Went Back Through the Backend<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#then-i-went-back-through-the-backend\" aria-label=\"Link to this section\">#</a></h2>\n<p>After React, I moved into Code the Dream's <strong>Node 26.2 Cohort</strong>, which ran from late March through June.</p>\n<p>Again, Node and Express were not new territory. I had already built full-stack applications and worked with the MERN stack, but going through a dedicated backend course gave me an opportunity to spend more time on the details instead of treating the backend as whatever needed to exist so the frontend could work.</p>\n<p>We spent time with routes, middleware, authentication, validation, error handling, HTTP semantics, database access, and application structure.</p>\n<p>One technology I got a lot more practice with was <strong>Prisma</strong>.</p>\n<p>I had used Prisma before CTD, but not enough that I would have called myself particularly comfortable with it. CTD gave me more opportunities to use it deliberately instead of simply following the happy path through an ORM.</p>\n<p>That distinction mattered.</p>\n<p>There are a lot of technologies I have <em>used</em>. That does not necessarily mean I understand all of the decisions they are making for me.</p>\n<p>Knowing how to use a tool is one level.</p>\n<p>Knowing what that tool is abstracting away is another.</p>\n<h2 id=\"then-came-practicum\">Then Came Practicum<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#then-came-practicum\" aria-label=\"Link to this section\">#</a></h2>\n<p>After finishing the coursework, I joined Code the Dream's <strong>Summer 2026 Practicum</strong>.</p>\n<p>This part felt familiar.</p>\n<p>I had already done several team projects through Chingu, so practicum was not my introduction to collaboration, Git, pull requests, requirements, product decisions, or the occasional joy of discovering two developers had spent an afternoon independently solving the same problem.</p>\n<p>What made practicum interesting was the amount of structure we had to build around the development itself.</p>\n<p>Our team built <a href=\"https://github.com/Code-the-Dream-School/summer-26-js-practicum-team2\"><strong>Sprout</strong></a>, a financial literacy application.</p>\n<p>The easiest way I have found to describe it is something like Duolingo pointed at personal finance: short lessons, quizzes, progress tracking, onboarding, and a guided learning path.</p>\n<p>The idea was to make a subject people know they <em>should</em> understand feel less like sitting down with a textbook, because almost nobody wakes up Saturday morning thinking:</p>\n<blockquote>\n<p>You know what would really make this weekend? Budgeting terminology.</p>\n</blockquote>\n<p>Sprout was not the project I would have chosen on my own, and I think that made the experience more interesting.</p>\n<p>When something is your personal project, you can build whatever you want. On a team, the product does not belong entirely to you. You have to build the thing the group agreed to build.</p>\n<p>That is a different skill.</p>\n<h2 id=\"before-we-built-much-of-anything-we-built-a-backlog\">Before We Built Much of Anything, We Built a Backlog<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#before-we-built-much-of-anything-we-built-a-backlog\" aria-label=\"Link to this section\">#</a></h2>\n<p>One of the first things I did was start planning.</p>\n<p>And then I planned some more.</p>\n<p>And then apparently I decided Jira had not suffered enough, because I built a pretty enormous backlog.</p>\n<p>There are still plenty of tickets sitting in it that we never touched, and that was intentional. A backlog is not a promise that everything gets built. It is a record of things that <em>could</em> get built.</p>\n<p>We split the core work across sprint weeks and broke larger ideas into epics, user stories, and smaller tasks. A Jira ticket would often become a GitHub issue. That issue gave us something concrete to build around and could then map to a branch.</p>\n<p>Eventually there was a clear line connecting:</p>\n<p><strong>idea → epic → story → issue → branch → pull request → application</strong></p>\n<p>That process became one of the more valuable parts of practicum for me because a large application does not get built by somebody opening an editor and thinking:</p>\n<blockquote>\n<p>Today I shall implement the application.</p>\n</blockquote>\n<p>You build a small piece. Then another. Then another.</p>\n<p>Sometimes several developers were contributing to the same larger feature, so we would use what amounted to a parent or integration branch. Instead of everybody immediately merging their work into <code>development</code>, we could combine related work there first, get the pieces cooperating, fix whatever exploded, and then merge the larger feature.</p>\n<p>It added another layer to the Git workflow, but it also prevented <code>development</code> from becoming quite as adventurous.</p>\n<h2 id=\"we-never-really-finished-an-epic\">We Never Really \"Finished\" an Epic<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#we-never-really-finished-an-epic\" aria-label=\"Link to this section\">#</a></h2>\n<p>Something else became apparent pretty quickly: epics are big.</p>\n<p>Really big.</p>\n<p>When you only have a limited number of weeks, \"complete the epic\" is often the wrong goal. So we sliced useful pieces out of them instead.</p>\n<p>One week we might improve onboarding. Later, we would come back and add something else. The learning path grew incrementally. Admin tools grew incrementally. Accounts grew incrementally. Testing grew incrementally.</p>\n<p>Even now, I do not think I could point at many of our larger epics and say:</p>\n<blockquote>\n<p>Yep. Finished forever.</p>\n</blockquote>\n<p>There are still stories sitting in the backlog, but the application works.</p>\n<p>That taught me something useful about MVP development. An epic does not have to be complete for part of it to create value. Sometimes the right feature is simply the smallest useful slice of a much larger idea.</p>\n<p>In a strange way, I suppose Baby Book had already taught me a smaller version of that lesson.</p>\n<p>It never became the application I originally imagined, but for a brief period the pieces we actually needed were useful.</p>\n<p>Sometimes software has already done its job before you finish everything you once imagined for it.</p>\n<h2 id=\"then-sprout-became-complicated\">Then Sprout Became Complicated<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#then-sprout-became-complicated\" aria-label=\"Link to this section\">#</a></h2>\n<p>Sprout used React on the frontend, Node and Express on the backend, and MongoDB underneath. We used Tailwind CSS and eventually created a collection of shared components so the UI could stay relatively consistent while different developers worked on different screens.</p>\n<p>That became important very quickly.</p>\n<p>Without shared components, five developers can somehow invent seven buttons. Each one is slightly different; none of them are necessarily wrong, and together they make the application look like it was assembled by committee.</p>\n<p>Because it was.</p>\n<p>Shared components gave us some guardrails, but the more interesting architectural decisions happened around the curriculum.</p>\n<p>Several members of the team worked together to build our learning material into structured JSON, and a huge part of the application eventually revolved around that structure: lessons, sections, quizzes, questions, answers, and progress.</p>\n<p>The JSON became something close to a content engine for the application.</p>\n<p>For a while, that worked really well.</p>\n<p>Then we started asking uncomfortable questions. Questions like:</p>\n<blockquote>\n<p>Should the correct answers to a quiz really be sitting somewhere the browser can inspect?</p>\n</blockquote>\n<p>Probably not.</p>\n<h2 id=\"one-architecture-decision-created-a-whole-new-category-of-bugs\">One Architecture Decision Created a Whole New Category of Bugs<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#one-architecture-decision-created-a-whole-new-category-of-bugs\" aria-label=\"Link to this section\">#</a></h2>\n<p>Eventually we started integrating more of the curriculum data with MongoDB.</p>\n<p>Part of the motivation was protecting quiz answers. If the browser receives everything—including which answer is correct—then somebody who knows their way around developer tools can potentially skip the whole \"learning\" portion of the learning application.</p>\n<p>Which feels slightly against the spirit of the thing.</p>\n<p>Moving more responsibility to the backend made sense, but architectural decisions rarely arrive alone. This one brought friends: more requests, more synchronization, more state, more opportunities for the frontend and backend to disagree, performance concerns, and some really annoying bugs.</p>\n<p>We solved one problem and introduced an entirely different class of problems.</p>\n<p>That does not necessarily mean the decision was wrong. It means the decision had a cost, and that might be one of the most useful things Sprout reinforced for me.</p>\n<p>Architecture is usually not:</p>\n<blockquote>\n<p>Which solution is correct?</p>\n</blockquote>\n<p>It is more often:</p>\n<blockquote>\n<p>Which set of problems would you rather have?</p>\n</blockquote>\n<p>I may revisit that part of Sprout in the future because I think there are better ways we could protect assessment data without making parts of the experience as expensive or fragile as they became.</p>\n<p>But that is also why I am glad we tried it.</p>\n<p>You learn a lot more from an architectural decision that creates unexpected problems than one that only exists in a diagram.</p>\n<h2 id=\"testing-was-only-half-of-what-i-enjoyed\">Testing Was Only Half of What I Enjoyed<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#testing-was-only-half-of-what-i-enjoyed\" aria-label=\"Link to this section\">#</a></h2>\n<p>Testing became a major part of my role on the project. I worked with <strong>Jest, Vitest, React Testing Library, and Playwright</strong>, and I liked the way testing forced us to think beyond the exact sequence of events we pictured when writing a feature.</p>\n<p>What happens if the component unmounts? What happens if the network request fails? What if authentication has not finished hydrating? What if somebody double-clicks? What if the frontend thinks a lesson is complete and the backend disagrees?</p>\n<p>We found all of those.</p>\n<p>But something else ended up being just as interesting to me:</p>\n<p><strong>CI.</strong></p>\n<p>I really enjoyed working with <strong>GitHub Actions</strong> and getting our tests running automatically as part of the development workflow.</p>\n<p>There is something deeply satisfying about turning:</p>\n<blockquote>\n<p>Somebody should probably run the tests before we merge this.</p>\n</blockquote>\n<p>into:</p>\n<blockquote>\n<p>The repository literally will not let us forget.</p>\n</blockquote>\n<p>That felt like another step beyond simply knowing how to write tests.</p>\n<p>A test suite sitting on somebody's laptop is useful. A test suite that runs automatically against changes is infrastructure.</p>\n<p>Once I started working on that side of the project, I found myself liking it quite a bit: tests, coverage, automated checks, GitHub Actions, deployment workflows, and all the little pieces that make it harder for a team to accidentally ship something broken.</p>\n<p>None of those things are particularly flashy.</p>\n<p>But they make everything else easier to trust.</p>\n<h2 id=\"some-bugs-deserve-their-own-zip-code\">Some Bugs Deserve Their Own ZIP Code<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#some-bugs-deserve-their-own-zip-code\" aria-label=\"Link to this section\">#</a></h2>\n<p>We still had plenty of bugs.</p>\n<p>Some were straightforward, friendly bugs. The sort that basically introduce themselves:</p>\n<blockquote>\n<p>Hello. I am on line 47.</p>\n</blockquote>\n<p>Those are wonderful.</p>\n<p>Others required the application to be in exactly the wrong state at exactly the right time.</p>\n<p>We had authentication behavior where the frontend could decide where a user should be redirected before it had finished determining whether the user was authenticated. Everything worked unless the timing changed, which is always comforting.</p>\n<p>We had asynchronous quiz operations where work could finish after the component that started it no longer existed. We had buttons users could click multiple times while a request was still running. We had lesson progression and quiz completion logic with occasionally incompatible definitions of reality.</p>\n<p>Some of those bugs looked completely fine when you clicked through the application manually.</p>\n<p>Then an automated test created the right sequence of events and everything fell apart.</p>\n<p>Beautiful.</p>\n<p>Those are the problems I enjoy the most now: the ones where the solution requires understanding the system rather than simply finding a typo.</p>\n<h2 id=\"git-got-more-interesting-too\">Git Got More Interesting Too<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#git-got-more-interesting-too\" aria-label=\"Link to this section\">#</a></h2>\n<p>Working by yourself gives you a very misleading impression of Git.</p>\n<p>You make a branch, write some code, commit it, merge it, and suddenly you feel pretty good about version control.</p>\n<p>Working with several developers changes the situation, especially once branches start depending on other branches.</p>\n<p>Suddenly Git becomes less:</p>\n<blockquote>\n<p>Save my work.</p>\n</blockquote>\n<p>and more:</p>\n<blockquote>\n<p>Which version of this application represents reality right now?</p>\n</blockquote>\n<p>The parent-branch strategy helped with that. So did smaller pull requests, better issues, and simply talking to each other before two people changed the same area of the application.</p>\n<p>That is the funny thing about Git problems.</p>\n<p>A surprising number of them are actually communication problems that happen to eventually produce merge conflicts.</p>\n<h2 id=\"practicum-felt-familiar-because-of-chingu\">Practicum Felt Familiar Because of Chingu<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#practicum-felt-familiar-because-of-chingu\" aria-label=\"Link to this section\">#</a></h2>\n<p>A lot of practicum reminded me of <strong>Chingu Voyages</strong>, which makes sense because I had already done several Voyages as both a developer and a Product Owner.</p>\n<p>Chingu had already taught me that two developers can read the same requirement and walk away with completely different applications in their heads. It had already shown me how important communication becomes once multiple people share a repository, and it had already exposed me to team roles and product planning.</p>\n<p>The two programs just approached those experiences differently.</p>\n<h2 id=\"chingu-does-a-great-job-simulating-a-product-team\">Chingu Does a Great Job Simulating a Product Team<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#chingu-does-a-great-job-simulating-a-product-team\" aria-label=\"Link to this section\">#</a></h2>\n<p>One thing I genuinely like about Chingu is that a Voyage can include several different roles.</p>\n<p>Developers may work alongside a <strong>Product Owner, Scrum Master, and UI/UX Designer</strong>, which creates some separation between responsibilities. A designer is concentrating on how something should look and behave. A Product Owner is shaping priorities and requirements. A Scrum Master helps keep the process moving.</p>\n<p>Developers then have to take those ideas and make them real.</p>\n<p>And eventually everybody discovers that a button somebody drew in thirty seconds somehow requires half a day to implement correctly.</p>\n<p>That structure is useful.</p>\n<p>The challenge, particularly in some lower tiers, is that experience levels among developers can vary quite a bit. Everybody might technically satisfy the requirements for the Voyage while having very different levels of comfort with JavaScript, Git, architecture, APIs, or debugging.</p>\n<p>Sometimes you discover those differences while trying to ship something.</p>\n<p>Which, to be fair, is also pretty realistic.</p>\n<h2 id=\"code-the-dream-gave-us-a-different-baseline\">Code the Dream Gave Us a Different Baseline<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#code-the-dream-gave-us-a-different-baseline\" aria-label=\"Link to this section\">#</a></h2>\n<p>Code the Dream's practicum had another advantage: everyone had already completed defined coursework.</p>\n<p>That did not make everyone's skills identical—not even close—but it gave us shared reference points. We had all traveled through at least some of the same material before arriving at the project.</p>\n<p>The bigger difference was that we did not have every traditional product role separated out. We were mostly developers, so we had to wear more hats ourselves.</p>\n<p>Someone had to think about product. Someone had to think about design. Someone had to organize the work and maintain the backlog. Someone had to worry about whether CI was working.</p>\n<p>Usually everybody ended up doing more than whatever role they originally imagined themselves doing.</p>\n<p>And I think that was good for us.</p>\n<h2 id=\"the-people-part\">The People Part<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#the-people-part\" aria-label=\"Link to this section\">#</a></h2>\n<p><a href=\"https://www.linkedin.com/in/berenice-rojas\"><strong>Berenice</strong></a> stepped in beside me as a co-project lead, and that helped me more than I probably said at the time. She even played a role in designing the UI/UX.</p>\n<p>She gave me somebody to bounce ideas off of while we built epics, shaped user stories, created tasks, organized Jira, and tried to decide which pieces actually mattered enough to belong in our MVP.</p>\n<p>Berenice and I worked together to decide which parts were actually critical for an MVP. Those moved forward; the rest could wait.</p>\n<p>There are a lot of small judgment calls hidden inside project planning. Having another person there to challenge an idea—or confirm that I was not completely overthinking it—made that a lot easier.</p>\n<p><a href=\"http://www.linkedin.com/in/kristen-wishart\"><strong>Kristen</strong></a> and <a href=\"https://github.com/BytesOfStrength\"><strong>Maryzabeth</strong></a> took on another enormous part of the application.</p>\n<p>They worked together on our initial low-fidelity wireframes, which became high-fidelity wireframes and eventually the foundation for the UI and UX. Then they worked together on developing the curriculum that became the content driving much of Sprout.</p>\n<p>Kristen had a particularly good instinct for keeping us connected to the original vision. Software developers are exceptionally talented at taking a reasonable idea and turning it into fourteen features, three dashboards, a notification system, and possibly a social network.</p>\n<p>Sometimes somebody has to say:</p>\n<blockquote>\n<p>We probably do not need that.</p>\n</blockquote>\n<p>Kristen was good at that. A lot of her vision made it into the final product precisely because she was willing to pull us back toward the application we had actually agreed to build.</p>\n<p>Maryzabeth became one of those people every remote team quietly depends on. She contributed across both frontend and backend work, but she also became our unofficial secretary.</p>\n<p>She took notes, documented decisions, and if somebody missed a meeting or standup, she was usually the person making sure they knew what happened.</p>\n<p>That sounds like a small thing until you work on a distributed team.</p>\n<p>Then you discover that keeping everybody operating from the same version of reality is basically a feature of its own.</p>\n<p>Everybody ended up bringing something beyond code, and that is the part of team development I find most interesting.</p>\n<h2 id=\"we-also-had-actual-adults-in-the-room\">We Also Had Actual Adults in the Room<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#we-also-had-actual-adults-in-the-room\" aria-label=\"Link to this section\">#</a></h2>\n<p>Code the Dream pairs practicum teams with mentors, and our team had <a href=\"https://www.linkedin.com/in/hectorfgonzalez/\"><strong>Hector</strong></a> and <a href=\"https://www.linkedin.com/in/mntri4/\"><strong>Mario</strong></a>.</p>\n<p>Hector brought real-world software development experience into our conversations, while Mario brought a lot of optimism and was always willing to listen while we worked our way through something.</p>\n<p>What helped most was rarely somebody simply handing us an answer. Usually it was somebody with more experience asking a question we had not asked ourselves:</p>\n<blockquote>\n<p>Why did you build it this way?</p>\n</blockquote>\n<p>Sometimes we had a very good answer.</p>\n<p>Sometimes the answer turned out to be:</p>\n<blockquote>\n<p>Because that is how it currently is.</p>\n</blockquote>\n<p>Those conversations were useful too.</p>\n<h2 id=\"so-what-actually-changed\">So What Actually Changed?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#so-what-actually-changed\" aria-label=\"Link to this section\">#</a></h2>\n<p>This is probably where I would describe the year differently than I would have twelve months ago.</p>\n<p>I did not learn that teams exist. I had already worked on teams. I did not learn how to build a full-stack application. I had already built them. I did not discover MERN. I already knew it.</p>\n<p>What changed was the level at which I started thinking about all of those things.</p>\n<p>I understand React Context better than I did. I am much more comfortable with testing. I have more practical experience with Prisma. I learned more about CI and GitHub Actions than I expected to, and I got more interested in the infrastructure surrounding the code.</p>\n<p>I spent more time thinking about how work gets broken down before anyone writes it. I became more deliberate about branch strategies when several pieces of one feature were being developed at the same time. I watched an architectural decision solve one problem while creating three new ones, and I saw how much a shared component library matters when several developers are building the same UI.</p>\n<p>More than anything, I got another reminder that software development is full of tradeoffs disguised as decisions.</p>\n<p>Maybe that is the biggest thing I got out of this year.</p>\n<p>I did not necessarily add another stack to the list of technologies I know.</p>\n<p>I got better at seeing the system around them.</p>\n<h2 id=\"the-other-lesson-had-nothing-to-do-with-code\">The Other Lesson Had Nothing to Do With Code<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#the-other-lesson-had-nothing-to-do-with-code\" aria-label=\"Link to this section\">#</a></h2>\n<p>I also learned that progress does not require the schedule I thought it did.</p>\n<p>For a long time, I pictured serious development work happening in large, uninterrupted blocks: a free Saturday, several hours at a desk, headphones on, everything lined up.</p>\n<p>That sounds wonderful.</p>\n<p>It describes almost none of what the past year looked like.</p>\n<p>A lot of this year happened in pieces. Thirty minutes here. An hour after bedtime. A bug before dinner. A pull request during nap time. Reading yesterday's notes because I had absolutely no idea what the version of me from forty-eight hours ago had been thinking.</p>\n<p>And somehow it still added up.</p>\n<p>I used to think real progress required long stretches of uninterrupted time. It turns out a surprising amount of it comes from getting good at returning.</p>\n<p>Open the laptop. Find the note. Read the issue. Make the next change. Commit it. Come back later.</p>\n<p>That is not the montage version of learning software development.</p>\n<p>It is probably a lot closer to the real one.</p>\n<h2 id=\"the-part-that-made-any-of-this-possible\">The Part That Made Any of This Possible<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#the-part-that-made-any-of-this-possible\" aria-label=\"Link to this section\">#</a></h2>\n<p>Outside the project, my wife <a href=\"https://www.linkedin.com/in/jayanti-hari/\"><strong>Jayanti</strong></a> listened to far more discussion about failed tests, authentication bugs, GitHub Actions, pull requests, MongoDB, Render deployments, and merge conflicts than any reasonable marriage contract should require.</p>\n<p>She was also there through the much more important part of the year while we figured out how to become parents together.</p>\n<p>That included the unfinished Baby Book, the last-minute contraction feature, sleep schedules that were mostly theoretical, and plenty of development sessions squeezed into whatever part of the day happened to survive.</p>\n<p>It has been a year.</p>\n<p>A really good one, a very busy one, and often a very tired one.</p>\n<h2 id=\"so-what-s-next\">So... What's Next?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#so-what-s-next\" aria-label=\"Link to this section\">#</a></h2>\n<p>Practicum has wrapped.</p>\n<p>I may keep tinkering with Sprout. There are still architectural decisions I want to revisit, experiments I want to try, and a Jira backlog large enough to keep several imaginary development teams busy.</p>\n<p>But I am also ready to build other things.</p>\n<p>I have a Rust game I keep coming back to. I want to contribute to open source properly. Baby Book is still sitting somewhere in the back of my head, too.</p>\n<p>And I have a growing list of things from this past year that deserve articles of their own: CI pipelines, testing patterns, authentication races, architecture mistakes, Prisma, React, Rust, and the sort of problems where the first draft of the article usually begins with:</p>\n<blockquote>\n<p>I thought this would be easy.</p>\n</blockquote>\n<p>Those are usually the interesting ones.</p>\n<p>I have also applied to <strong>Code the Dream's apprenticeship program</strong>.</p>\n<p>I do not know yet whether I will get in, and strangely, I am okay leaving that part of the story there.</p>\n<p>Not knowing.</p>\n<p>Because that is actually where I am.</p>\n<p>There is a tendency when writing about learning software development to wait until you know how the story ends. Get the job. Finish the project. Earn the certificate. Then go back and write the neat version where everything looks like it was leading somewhere obvious.</p>\n<p>Real life rarely gives you that version while you are living it.</p>\n<p>Right now, I have finished one thing and I am waiting to find out what comes next. Maybe that is the apprenticeship. Maybe it is a job. Maybe it is open source.</p>\n<p>Maybe I disappear into Rust for six months and emerge with something deeply unnecessary.</p>\n<p>There are worse outcomes.</p>\n<p>What I do know is that I finally want to write again, and I have a lot more to write about than I did when this blog went quiet.</p>\n<p>So <em>Journey to Code</em> is back.</p>\n<p>Probably a little slower. Definitely with more interruptions.</p>\n<p>But I think I am okay with that now.</p>\n<p>The last year taught me that progress does not always look like moving quickly. Sometimes it just means you keep coming back.</p>\n<p>And, conveniently, I just did.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/what-a-long-strange-trip/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "What a Long, Strange Trip...\nCanonical: https://mnixblog.netlify.app/what-a-long-strange-trip/\nA wedding, a newborn, Code the Dream, Sprout, and a year of commits I never quite found time to write about.\nThe last time I posted was on April 28, 2025. I know the exact date because it was the same day I got married.\nLooking back, that might explain why this blog settled down soon after.\nNot because I stopped programming. I was still building, learning, and working through plenty of code. I just quit writing about it.\nAs time passed, the blog seemed to drift further down the backlog. When I found myself with some free time, I was more likely to spend it squashing bugs, working on a project, or learning something new than writing about any of it.\nSo before I start writing about what comes next, I should probably explain what has happened since then.\nBecause it was a year.\nFirst, a tiny detail I forgot to mention\nWhile participating in a Chingu (https://chingu.io) Voyage, I met my wife.\nYes, that Chingu.\nThe online platform that put together teams of software engineers and agile leaders to build projects suitable to their abilities, argue politely about Git branches, and every once in a while spend twenty minutes figuring out who forgot to pull before opening a PR.\nBefore this past year, I had completed several Chingu Voyages, working as a developer and a product owner, and even as a mentor. One of those Voyages just happened to work out particularly well.\nI met her.\nAs far as I am aware, almost nobody over there knows about it. It never really came up, and there never seemed to be a natural moment in a Discord channel to mention it. Plus it all escalated well after that Voyage ended.\nThis blog has always been about my experience writing code and sharing the lessons I've learned in order to help other developers grow and tap into their potential.\nSo this may seem like it comes out of left field, but Chingu gave me teammates, portfolio projects, experience working on software teams...\n...and a wife.\nMaybe not in that order.\nYou might say that it was a successful Voyage.\nBefore the Baby, There Was Baby Book\nBefore summer could even arrive, we found out our lives were about to change again!\nWe found out we were expecting a son, due November 7. We began to think about all of the little things that come with two people becoming parents.\nNaturally, we both looked at each other and said:\nWe could probably build an app for this.\nSo we began working on an idea we called Baby Book.\nIn the first phase, it was only a household task and grocery list tracking app. The long-term plan was to track milestones, achievements, appointments, and eventually the little things one parent might miss while the other was there—so we could stay on the same page.\nMore or less, we set out to build a shared record of our new favorite person's life as it began to unfold.\nWe never did polish the application to the original design plan, but for a little while it was actually useful.\nWhen she went into labor, I threw together a quick feature for helping her with breathing and contractions. When our son was born, Baby Book shifted toward the realities of parenthood. We started tracking feeding times and details, diaper changes, and little notes.\nIt did not take long before we did what most new parents probably do.\nWe fell into a rhythm and stopped stressing over those details as much.\nOur need for this app faded, and Baby Book quietly moved into the category titled:\nI should really come back to this someday.\nAnd I still may.\nSome of the original idea could still be useful today, especially around milestones and giving us an easy way to share the little things the other parent might miss.\nBut it is not exactly at the top of the backlog right now.\nA Wild Son Has Appeared\nOur son was supposed to arrive on November 7.\nApparently, he had his own release schedule. He arrived October 12, about four weeks early.\nAnybody who has had an infant can probably relate to what happened to my schedule next. If you haven't, imagine handing your calendar to a tiny version of yourself that only speaks babble, uses his mouth to read, and thinks 3:17 in the morning is the perfect time to discuss politics.\nThat is roughly the system, right?\nThe funny part is that another fairly important date had happened only five days earlier.\nOn October 7, I was accepted into Code the Dream's React course.\nCode the Dream Was Already in Motion\nI had actually applied to Code the Dream (https://codethedream.org/) months earlier, around April 2025.\nThe timing of the whole thing could have been a little better. But it is not like I brought the baby home, learned a thing or two about our new tiny boss, and decided:\nYou know what this situation needs?\nAnother commitment.\nThe decision had already been made.\nI had tested out of the Intro to Programming course, which is how I ended up going directly into React.\nBy this point, I knew that it was going to be a challenge, but it seemed reasonable enough.\nStrangely enough, his early arrival may actually have made the start of the course easier. I lost some preparation time, but I gained a few weeks to settle into being a parent before React started. And that made a big difference.\nBy the time the React cohort started, I was not deciding whether I wanted to begin another programming course while raising a newborn. I had already committed to the course.\nNow I just had to figure out what continuing looked like under completely different circumstances.\nThe answer was very different from the way I had worked before.\nLong, uninterrupted coding sessions became harder to find. Development started happening in smaller windows: thirty minutes here, an hour there, whatever time happened to exist between everything else.\nIf twenty minutes of a thirty-five-minute session were spent trying to remember what I had been doing the last time I opened the project, that was a problem.\nSo I started leaving myself better notes. I broke problems into smaller pieces. I became more deliberate about commits and less interested in keeping an entire feature in my head.\nWithout really planning to, becoming a parent forced me to get better at stopping halfway through something while leaving enough breadcrumbs that future me could figure out what past me had been doing.\nAnd then React started.\nReact, the Second Time Around\nI joined Code the Dream's React v3 course as part of the Lark Cohort, which ran from November through February.\nCode the Dream was not where I first learned React, or where I first built a full-stack application. I had already built MERN applications, worked with APIs, Express, and MongoDB, and completed several development projects with other people through Chingu.\nWhat CTD gave me was something different.\nIt gave me several months to go back through technologies I already knew and find the places where my understanding was thinner than I realized.\nOne of those places was Context.\nI had used Context before and understood what it did, but there is a difference between knowing that Context exists and getting comfortable designing state around it, combining it with reducers, deciding what actually belongs there, and thinking about what happens as your component tree gets larger.\nSometimes learning something for the second time is a lot more useful than learning it the first time.\nThe first time, you are mostly trying to make it work.\nThe second time, you start asking why it works that way.\nThat became a recurring theme during the course. It was less:\nHere is React.\nand more:\nYou know React. Now let's find the parts you have been getting away with not understanding deeply enough.\nMy guided project grew throughout the course. Reducers came in, Context became more important, routing got more complicated, external data got involved, state crossed more boundaries, and testing became a larger part of the way I worked.\nEventually something I had built myself six weeks earlier could surprise me, which is always reassuring.\nSomewhere during that process, the question I asked after completing something changed.\nIt used to be:\nDoes it work?\nI would click around a few times, see that everything looked fine, and move on.\nNow I am much more likely to ask:\nOkay, it works. What breaks it?\nWhat happens if the request fails? What happens if the user refreshes here? What if this state has not hydrated yet? What happens if the component disappears before an asynchronous operation finishes? What happens if somebody clicks the same button five times while the first request is still running?\nThe happy path is still important.\nI just trust it a lot less than I used to.\nThen I Went Back Through the Backend\nAfter React, I moved into Code the Dream's Node 26.2 Cohort, which ran from late March through June.\nAgain, Node and Express were not new territory. I had already built full-stack applications and worked with the MERN stack, but going through a dedicated backend course gave me an opportunity to spend more time on the details instead of treating the backend as whatever needed to exist so the frontend could work.\nWe spent time with routes, middleware, authentication, validation, error handling, HTTP semantics, database access, and application structure.\nOne technology I got a lot more practice with was Prisma.\nI had used Prisma before CTD, but not enough that I would have called myself particularly comfortable with it. CTD gave me more opportunities to use it deliberately instead of simply following the happy path through an ORM.\nThat distinction mattered.\nThere are a lot of technologies I have used. That does not necessarily mean I understand all of the decisions they are making for me.\nKnowing how to use a tool is one level.\nKnowing what that tool is abstracting away is another.\nThen Came Practicum\nAfter finishing the coursework, I joined Code the Dream's Summer 2026 Practicum.\nThis part felt familiar.\nI had already done several team projects through Chingu, so practicum was not my introduction to collaboration, Git, pull requests, requirements, product decisions, or the occasional joy of discovering two developers had spent an afternoon independently solving the same problem.\nWhat made practicum interesting was the amount of structure we had to build around the development itself.\nOur team built Sprout (https://github.com/Code-the-Dream-School/summer-26-js-practicum-team2), a financial literacy application.\nThe easiest way I have found to describe it is something like Duolingo pointed at personal finance: short lessons, quizzes, progress tracking, onboarding, and a guided learning path.\nThe idea was to make a subject people know they should understand feel less like sitting down with a textbook, because almost nobody wakes up Saturday morning thinking:\nYou know what would really make this weekend? Budgeting terminology.\nSprout was not the project I would have chosen on my own, and I think that made the experience more interesting.\nWhen something is your personal project, you can build whatever you want. On a team, the product does not belong entirely to you. You have to build the thing the group agreed to build.\nThat is a different skill.\nBefore We Built Much of Anything, We Built a Backlog\nOne of the first things I did was start planning.\nAnd then I planned some more.\nAnd then apparently I decided Jira had not suffered enough, because I built a pretty enormous backlog.\nThere are still plenty of tickets sitting in it that we never touched, and that was intentional. A backlog is not a promise that everything gets built. It is a record of things that could get built.\nWe split the core work across sprint weeks and broke larger ideas into epics, user stories, and smaller tasks. A Jira ticket would often become a GitHub issue. That issue gave us something concrete to build around and could then map to a branch.\nEventually there was a clear line connecting:\nidea → epic → story → issue → branch → pull request → application\nThat process became one of the more valuable parts of practicum for me because a large application does not get built by somebody opening an editor and thinking:\nToday I shall implement the application.\nYou build a small piece. Then another. Then another.\nSometimes several developers were contributing to the same larger feature, so we would use what amounted to a parent or integration branch. Instead of everybody immediately merging their work into development, we could combine related work there first, get the pieces cooperating, fix whatever exploded, and then merge the larger feature.\nIt added another layer to the Git workflow, but it also prevented development from becoming quite as adventurous.\nWe Never Really \"Finished\" an Epic\nSomething else became apparent pretty quickly: epics are big.\nReally big.\nWhen you only have a limited number of weeks, \"complete the epic\" is often the wrong goal. So we sliced useful pieces out of them instead.\nOne week we might improve onboarding. Later, we would come back and add something else. The learning path grew incrementally. Admin tools grew incrementally. Accounts grew incrementally. Testing grew incrementally.\nEven now, I do not think I could point at many of our larger epics and say:\nYep. Finished forever.\nThere are still stories sitting in the backlog, but the application works.\nThat taught me something useful about MVP development. An epic does not have to be complete for part of it to create value. Sometimes the right feature is simply the smallest useful slice of a much larger idea.\nIn a strange way, I suppose Baby Book had already taught me a smaller version of that lesson.\nIt never became the application I originally imagined, but for a brief period the pieces we actually needed were useful.\nSometimes software has already done its job before you finish everything you once imagined for it.\nThen Sprout Became Complicated\nSprout used React on the frontend, Node and Express on the backend, and MongoDB underneath. We used Tailwind CSS and eventually created a collection of shared components so the UI could stay relatively consistent while different developers worked on different screens.\nThat became important very quickly.\nWithout shared components, five developers can somehow invent seven buttons. Each one is slightly different; none of them are necessarily wrong, and together they make the application look like it was assembled by committee.\nBecause it was.\nShared components gave us some guardrails, but the more interesting architectural decisions happened around the curriculum.\nSeveral members of the team worked together to build our learning material into structured JSON, and a huge part of the application eventually revolved around that structure: lessons, sections, quizzes, questions, answers, and progress.\nThe JSON became something close to a content engine for the application.\nFor a while, that worked really well.\nThen we started asking uncomfortable questions. Questions like:\nShould the correct answers to a quiz really be sitting somewhere the browser can inspect?\nProbably not.\nOne Architecture Decision Created a Whole New Category of Bugs\nEventually we started integrating more of the curriculum data with MongoDB.\nPart of the motivation was protecting quiz answers. If the browser receives everything—including which answer is correct—then somebody who knows their way around developer tools can potentially skip the whole \"learning\" portion of the learning application.\nWhich feels slightly against the spirit of the thing.\nMoving more responsibility to the backend made sense, but architectural decisions rarely arrive alone. This one brought friends: more requests, more synchronization, more state, more opportunities for the frontend and backend to disagree, performance concerns, and some really annoying bugs.\nWe solved one problem and introduced an entirely different class of problems.\nThat does not necessarily mean the decision was wrong. It means the decision had a cost, and that might be one of the most useful things Sprout reinforced for me.\nArchitecture is usually not:\nWhich solution is correct?\nIt is more often:\nWhich set of problems would you rather have?\nI may revisit that part of Sprout in the future because I think there are better ways we could protect assessment data without making parts of the experience as expensive or fragile as they became.\nBut that is also why I am glad we tried it.\nYou learn a lot more from an architectural decision that creates unexpected problems than one that only exists in a diagram.\nTesting Was Only Half of What I Enjoyed\nTesting became a major part of my role on the project. I worked with Jest, Vitest, React Testing Library, and Playwright, and I liked the way testing forced us to think beyond the exact sequence of events we pictured when writing a feature.\nWhat happens if the component unmounts? What happens if the network request fails? What if authentication has not finished hydrating? What if somebody double-clicks? What if the frontend thinks a lesson is complete and the backend disagrees?\nWe found all of those.\nBut something else ended up being just as interesting to me:\nCI.\nI really enjoyed working with GitHub Actions and getting our tests running automatically as part of the development workflow.\nThere is something deeply satisfying about turning:\nSomebody should probably run the tests before we merge this.\ninto:\nThe repository literally will not let us forget.\nThat felt like another step beyond simply knowing how to write tests.\nA test suite sitting on somebody's laptop is useful. A test suite that runs automatically against changes is infrastructure.\nOnce I started working on that side of the project, I found myself liking it quite a bit: tests, coverage, automated checks, GitHub Actions, deployment workflows, and all the little pieces that make it harder for a team to accidentally ship something broken.\nNone of those things are particularly flashy.\nBut they make everything else easier to trust.\nSome Bugs Deserve Their Own ZIP Code\nWe still had plenty of bugs.\nSome were straightforward, friendly bugs. The sort that basically introduce themselves:\nHello. I am on line 47.\nThose are wonderful.\nOthers required the application to be in exactly the wrong state at exactly the right time.\nWe had authentication behavior where the frontend could decide where a user should be redirected before it had finished determining whether the user was authenticated. Everything worked unless the timing changed, which is always comforting.\nWe had asynchronous quiz operations where work could finish after the component that started it no longer existed. We had buttons users could click multiple times while a request was still running. We had lesson progression and quiz completion logic with occasionally incompatible definitions of reality.\nSome of those bugs looked completely fine when you clicked through the application manually.\nThen an automated test created the right sequence of events and everything fell apart.\nBeautiful.\nThose are the problems I enjoy the most now: the ones where the solution requires understanding the system rather than simply finding a typo.\nGit Got More Interesting Too\nWorking by yourself gives you a very misleading impression of Git.\nYou make a branch, write some code, commit it, merge it, and suddenly you feel pretty good about version control.\nWorking with several developers changes the situation, especially once branches start depending on other branches.\nSuddenly Git becomes less:\nSave my work.\nand more:\nWhich version of this application represents reality right now?\nThe parent-branch strategy helped with that. So did smaller pull requests, better issues, and simply talking to each other before two people changed the same area of the application.\nThat is the funny thing about Git problems.\nA surprising number of them are actually communication problems that happen to eventually produce merge conflicts.\nPracticum Felt Familiar Because of Chingu\nA lot of practicum reminded me of Chingu Voyages, which makes sense because I had already done several Voyages as both a developer and a Product Owner.\nChingu had already taught me that two developers can read the same requirement and walk away with completely different applications in their heads. It had already shown me how important communication becomes once multiple people share a repository, and it had already exposed me to team roles and product planning.\nThe two programs just approached those experiences differently.\nChingu Does a Great Job Simulating a Product Team\nOne thing I genuinely like about Chingu is that a Voyage can include several different roles.\nDevelopers may work alongside a Product Owner, Scrum Master, and UI/UX Designer, which creates some separation between responsibilities. A designer is concentrating on how something should look and behave. A Product Owner is shaping priorities and requirements. A Scrum Master helps keep the process moving.\nDevelopers then have to take those ideas and make them real.\nAnd eventually everybody discovers that a button somebody drew in thirty seconds somehow requires half a day to implement correctly.\nThat structure is useful.\nThe challenge, particularly in some lower tiers, is that experience levels among developers can vary quite a bit. Everybody might technically satisfy the requirements for the Voyage while having very different levels of comfort with JavaScript, Git, architecture, APIs, or debugging.\nSometimes you discover those differences while trying to ship something.\nWhich, to be fair, is also pretty realistic.\nCode the Dream Gave Us a Different Baseline\nCode the Dream's practicum had another advantage: everyone had already completed defined coursework.\nThat did not make everyone's skills identical—not even close—but it gave us shared reference points. We had all traveled through at least some of the same material before arriving at the project.\nThe bigger difference was that we did not have every traditional product role separated out. We were mostly developers, so we had to wear more hats ourselves.\nSomeone had to think about product. Someone had to think about design. Someone had to organize the work and maintain the backlog. Someone had to worry about whether CI was working.\nUsually everybody ended up doing more than whatever role they originally imagined themselves doing.\nAnd I think that was good for us.\nThe People Part\nBerenice (https://www.linkedin.com/in/berenice-rojas) stepped in beside me as a co-project lead, and that helped me more than I probably said at the time. She even played a role in designing the UI/UX.\nShe gave me somebody to bounce ideas off of while we built epics, shaped user stories, created tasks, organized Jira, and tried to decide which pieces actually mattered enough to belong in our MVP.\nBerenice and I worked together to decide which parts were actually critical for an MVP. Those moved forward; the rest could wait.\nThere are a lot of small judgment calls hidden inside project planning. Having another person there to challenge an idea—or confirm that I was not completely overthinking it—made that a lot easier.\nKristen (http://www.linkedin.com/in/kristen-wishart) and Maryzabeth (https://github.com/BytesOfStrength) took on another enormous part of the application.\nThey worked together on our initial low-fidelity wireframes, which became high-fidelity wireframes and eventually the foundation for the UI and UX. Then they worked together on developing the curriculum that became the content driving much of Sprout.\nKristen had a particularly good instinct for keeping us connected to the original vision. Software developers are exceptionally talented at taking a reasonable idea and turning it into fourteen features, three dashboards, a notification system, and possibly a social network.\nSometimes somebody has to say:\nWe probably do not need that.\nKristen was good at that. A lot of her vision made it into the final product precisely because she was willing to pull us back toward the application we had actually agreed to build.\nMaryzabeth became one of those people every remote team quietly depends on. She contributed across both frontend and backend work, but she also became our unofficial secretary.\nShe took notes, documented decisions, and if somebody missed a meeting or standup, she was usually the person making sure they knew what happened.\nThat sounds like a small thing until you work on a distributed team.\nThen you discover that keeping everybody operating from the same version of reality is basically a feature of its own.\nEverybody ended up bringing something beyond code, and that is the part of team development I find most interesting.\nWe Also Had Actual Adults in the Room\nCode the Dream pairs practicum teams with mentors, and our team had Hector (https://www.linkedin.com/in/hectorfgonzalez/) and Mario (https://www.linkedin.com/in/mntri4/).\nHector brought real-world software development experience into our conversations, while Mario brought a lot of optimism and was always willing to listen while we worked our way through something.\nWhat helped most was rarely somebody simply handing us an answer. Usually it was somebody with more experience asking a question we had not asked ourselves:\nWhy did you build it this way?\nSometimes we had a very good answer.\nSometimes the answer turned out to be:\nBecause that is how it currently is.\nThose conversations were useful too.\nSo What Actually Changed?\nThis is probably where I would describe the year differently than I would have twelve months ago.\nI did not learn that teams exist. I had already worked on teams. I did not learn how to build a full-stack application. I had already built them. I did not discover MERN. I already knew it.\nWhat changed was the level at which I started thinking about all of those things.\nI understand React Context better than I did. I am much more comfortable with testing. I have more practical experience with Prisma. I learned more about CI and GitHub Actions than I expected to, and I got more interested in the infrastructure surrounding the code.\nI spent more time thinking about how work gets broken down before anyone writes it. I became more deliberate about branch strategies when several pieces of one feature were being developed at the same time. I watched an architectural decision solve one problem while creating three new ones, and I saw how much a shared component library matters when several developers are building the same UI.\nMore than anything, I got another reminder that software development is full of tradeoffs disguised as decisions.\nMaybe that is the biggest thing I got out of this year.\nI did not necessarily add another stack to the list of technologies I know.\nI got better at seeing the system around them.\nThe Other Lesson Had Nothing to Do With Code\nI also learned that progress does not require the schedule I thought it did.\nFor a long time, I pictured serious development work happening in large, uninterrupted blocks: a free Saturday, several hours at a desk, headphones on, everything lined up.\nThat sounds wonderful.\nIt describes almost none of what the past year looked like.\nA lot of this year happened in pieces. Thirty minutes here. An hour after bedtime. A bug before dinner. A pull request during nap time. Reading yesterday's notes because I had absolutely no idea what the version of me from forty-eight hours ago had been thinking.\nAnd somehow it still added up.\nI used to think real progress required long stretches of uninterrupted time. It turns out a surprising amount of it comes from getting good at returning.\nOpen the laptop. Find the note. Read the issue. Make the next change. Commit it. Come back later.\nThat is not the montage version of learning software development.\nIt is probably a lot closer to the real one.\nThe Part That Made Any of This Possible\nOutside the project, my wife Jayanti (https://www.linkedin.com/in/jayanti-hari/) listened to far more discussion about failed tests, authentication bugs, GitHub Actions, pull requests, MongoDB, Render deployments, and merge conflicts than any reasonable marriage contract should require.\nShe was also there through the much more important part of the year while we figured out how to become parents together.\nThat included the unfinished Baby Book, the last-minute contraction feature, sleep schedules that were mostly theoretical, and plenty of development sessions squeezed into whatever part of the day happened to survive.\nIt has been a year.\nA really good one, a very busy one, and often a very tired one.\nSo... What's Next?\nPracticum has wrapped.\nI may keep tinkering with Sprout. There are still architectural decisions I want to revisit, experiments I want to try, and a Jira backlog large enough to keep several imaginary development teams busy.\nBut I am also ready to build other things.\nI have a Rust game I keep coming back to. I want to contribute to open source properly. Baby Book is still sitting somewhere in the back of my head, too.\nAnd I have a growing list of things from this past year that deserve articles of their own: CI pipelines, testing patterns, authentication races, architecture mistakes, Prisma, React, Rust, and the sort of problems where the first draft of the article usually begins with:\nI thought this would be easy.\nThose are usually the interesting ones.\nI have also applied to Code the Dream's apprenticeship program.\nI do not know yet whether I will get in, and strangely, I am okay leaving that part of the story there.\nNot knowing.\nBecause that is actually where I am.\nThere is a tendency when writing about learning software development to wait until you know how the story ends. Get the job. Finish the project. Earn the certificate. Then go back and write the neat version where everything looks like it was leading somewhere obvious.\nReal life rarely gives you that version while you are living it.\nRight now, I have finished one thing and I am waiting to find out what comes next. Maybe that is the apprenticeship. Maybe it is a job. Maybe it is open source.\nMaybe I disappear into Rust for six months and emerge with something deeply unnecessary.\nThere are worse outcomes.\nWhat I do know is that I finally want to write again, and I have a lot more to write about than I did when this blog went quiet.\nSo Journey to Code is back.\nProbably a little slower. Definitely with more interruptions.\nBut I think I am okay with that now.\nThe last year taught me that progress does not always look like moving quickly. Sometimes it just means you keep coming back.\nAnd, conveniently, I just did.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/what-a-long-strange-trip/#ob-discussion)",
      "date_modified": "2026-09-20T16:00:18Z",
      "date_published": "2026-09-19T14:57:16Z",
      "id": "https://mnixblog.netlify.app/what-a-long-strange-trip/",
      "image": "https://mnixblog.netlify.app/images/what-a-long-strange-trip/cover.png",
      "summary": "After a year away, I’m back with lessons from parenthood, Code the Dream, React, Node.js, testing, teamwork, and learning to keep coming back.",
      "tags": [
        "Frameworks",
        "Teamwork",
        "Career"
      ],
      "title": "What a Long, Strange Trip...",
      "url": "https://mnixblog.netlify.app/what-a-long-strange-trip/"
    },
    {
      "content_html": "<p>I grew up as the youngest offspring of one particularly tech-savvy individual who among many jobs, initially moved our family from many places like Texas, Florida, North Carolina, and eventually to West Virginia where I was raised as a child. The area in particular that I grew up is very rural, and essentially a decade or more behind modern times. However in our household this was almost uncertainly not the case. Since my father was such a connoisseur of technology, it's basically an inherited trait to me. For a little bit of a backstory, we wound up here as my father was writing assembly language code that would help facilitate the operation of coal power plants. So, naturally we always had the latest and greatest trends.</p>\n<p>Because of the available computing power at my fingertips I was often times able to experiment well before I should have with hardware. In our family, my dad would always upgrade his PC almost annually to the next latest and greatest system. One of my very first computers was a 286 processor with 16mhz. This introduced me to the world of the command line. I learned early how to navigate and traverse folders using Disk Operating System (or DOS). I learned the <code>del *.*</code> command at some point. At least one time or two, this got me into trouble. At home, it wasn't as big of a deal. After deleting DOS from my root directory once or twice, my dad decided to install his own standalone operating system for me that never let me reach the command line, instead I could only pick what game to play and it would type the commands for me.</p>\n<h2 id=\"the-tech-hand-me-down-dynasty\">The Tech Hand-Me-Down Dynasty<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-tech-hand-me-down-dynasty\" aria-label=\"Link to this section\">#</a></h2>\n<p>Our household had its own tech ecosystem, with my dad at the top of the pyramid. He'd get the cutting-edge system, then his previous machine would go to my oldest brother, whose computer would then go to my middle brother, which meant I'd inherit whatever my middle brother was using. Being the youngest in a tech-obsessed family had its perks, even if I was always three generations behind the curve.</p>\n<p>When Dad upgraded to his first Pentium running at a mind-blowing 100MHz, it kicked off a beautiful cascade of hand-me-downs: oldest brother got Dad's 486 DX2 with 66MHz, middle brother received the 486DX with 33MHz, and I finally graduated to the 386 with 33MHz. Each time this happened, it felt like Christmas morning, even though I was essentially getting the technological equivalent of my brothers' worn-out sneakers.</p>\n<p>Living in a rural West Virginia town where the public schools didn't even connect to the internet until after 2000, our home stood out like a digital oasis in a technological desert. My friends would visit and stare in wonder at our setup. Their jaws would hit the floor when they realized we had dedicated phone lines just for internet—a luxury in the era of dial-up when most families had to choose between making calls or going online.</p>\n<p>Dad wasn't just tech-savvy; he ran a dial-up internet service provider. This meant we were always on the cutting edge, while my school was still using computers that might as well have been powered by hamster wheels. I never once experienced the frustration of having someone pick up the phone while I was online—a common trauma for most 90s kids.</p>\n<h2 id=\"doom-lan-parties-and-the-cruel-reality-of-crashed-map-editors\">DOOM, LAN Parties, and the Cruel Reality of Crashed Map Editors<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#doom-lan-parties-and-the-cruel-reality-of-crashed-map-editors\" aria-label=\"Link to this section\">#</a></h2>\n<p>The mid-90s were the golden age of DOOM, and our basement became command central for epic LAN parties around 1995. Back then, Windows couldn't hold a candle to DOS when it came to gaming performance. We would gather, connect our machines, and blast demons (or each other in deathmatch) for hours on end.</p>\n<p>But we weren't just players—we were creators too. The DOOM modding scene was where I cut my teeth on level design, spending countless hours crafting custom maps... only to have my underpowered 386 crash mid-creation, erasing everything I'd done. I can still feel that particular brand of heartbreak that comes from losing hours of creative work to the digital void. Nothing teaches you to save your work like losing an entire afternoon's worth of meticulously placed monsters and secret rooms.</p>\n<p>Storage in our house was a sight to behold. We literally had hundreds of 5.25\" floppy disks stashed everywhere—loaded with games, programming languages, and applications from computing's early days. These massive, actually-floppy disks are probably still boxed up somewhere in my parents' house, a physical museum of software history.</p>\n<p>I still remember the technological disappointment of our first 1x CD-ROM drive. On paper, it could hold way more data than any floppy. In practice? I discovered firsthand that it was often faster to install Windows using 64 separate 1.44\" floppy disks than waiting for that sluggish CD to do its thing. The only advantage was being able to start the installation and walk away instead of sitting there feeding in disk after disk like some sort of human vending machine.</p>\n<h2 id=\"the-windows-product-key-savant\">The Windows Product Key Savant<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-windows-product-key-savant\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here's a weird party trick I still have: I can recite the Windows 95 and 98 product keys from memory. Seriously. After installing these operating systems dozens of times (usually after breaking something), those alphanumeric sequences are permanently etched into my brain alongside my social security number and childhood phone number.</p>\n<p>Was it strictly kosher to use the same key repeatedly? Probably not. But in those days, activation was simpler, and we were just kids excited to get our machines back up and running after whatever catastrophe had befallen them (usually self-inflicted).</p>\n<h2 id=\"accidental-webmaster-and-the-great-apache-incident\">Accidental Webmaster and the Great Apache Incident<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#accidental-webmaster-and-the-great-apache-incident\" aria-label=\"Link to this section\">#</a></h2>\n<p>While most kids were content playing Nintendo, my brothers and I were diving headfirst into the wild west of early web development. We honed our skills on Geocities and AngelFire, painstakingly writing HTML in Notepad and proudly publishing pages that were essentially 99% animated GIFs and \"Under Construction\" banners.</p>\n<p>When Dad discovered our interest, he kicked things up several notches—purchasing us a domain name, setting up a web server, and giving each of us our own subdomain. Suddenly we graduated from Notepad to the \"professional-grade\" Microsoft FrontPage and were learning about mysterious concepts like FTP and hosting. These were the early days of web development. JavaScript was not nearly as powerful as it has become.</p>\n<p>One of my more notorious tech moments came when I accidentally installed Apache on my PC and started hosting a web server without even realizing what I'd done. Dad went into full-on security panic mode, convinced we'd been hacked, only to discover it was just his clueless youngest son experimenting with software he didn't understand. When he demanded to know why I'd done it, I genuinely had no idea what he was talking about—I didn't even know what Apache was! I'd just clicked through some install prompts and suddenly our home network had an unexpected web server. The digital equivalent of accidentally building a shed in the backyard.</p>\n<h2 id=\"emulation-console-gaming-on-pc-terms\">Emulation: Console Gaming on PC Terms<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#emulation-console-gaming-on-pc-terms\" aria-label=\"Link to this section\">#</a></h2>\n<p>While my friends were saving up allowance money for GameBoy cartridges and Super Nintendo games, we were exploring the fascinating world of emulation. Why play Super Mario on a tiny GameBoy screen when you could play it on a computer monitor with the ability to save anywhere and apply cheats without those clunky GameShark devices?</p>\n<p>It wasn't that we didn't have consoles—our gaming collection included everything from ancient Ataris and Commodore 64s to Nintendo, Super Nintendo, Genesis, Sega CD, and N64. But the flexibility of emulation gave us superpowers that console gamers could only dream of.</p>\n<p>The crown jewel of our portable gaming collection wasn't even a Nintendo product—it was the Sega Nomad, a handheld that played full Genesis cartridges with a backlit screen when that was basically unheard of technology. This thing was truly revolutionary—a fully backlit portable console playing 16-bit games when the GameBoy was still rocking monochrome graphics and requiring external light sources.</p>\n<h2 id=\"breaking-free-my-first-custom-build\">Breaking Free: My First Custom Build<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#breaking-free-my-first-custom-build\" aria-label=\"Link to this section\">#</a></h2>\n<p>Fast forward to my teenage years. I was finally ready to break free from the hand-me-down cycle and build my very first PC from scratch. Spoiler alert: it didn't go smoothly.</p>\n<p>My first motherboard purchase ended with the vendor vanishing faster than my motivation to do homework. Still salty about that one, but it taught me an early lesson about researching vendors before handing over cash.</p>\n<p>Eventually, I assembled what felt like a supercomputer at the time: a Pentium 4 Dual Core running at 1.2GHz paired with some NVIDIA graphics card whose model number has long since faded from memory. But it ran World of Warcraft on maximum settings, and that's all that mattered to teenage me.</p>\n<h2 id=\"the-brother-borrowing-incident\">The Brother \"Borrowing\" Incident<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-brother-borrowing-incident\" aria-label=\"Link to this section\">#</a></h2>\n<p>When I moved out west, my computer stayed behind. At some point, my brother decided my PC looked lonely and \"borrowed\" it without asking (as brothers do). Then Christmas rolled around, and guess what appeared under the tree with my name on it? MY OWN PC. With a blown power supply.</p>\n<p>I never did replace that power supply. The PC sat gathering dust, a monument to sibling shenanigans and the importance of surge protectors.</p>\n<h2 id=\"the-corporate-castoff-renaissance\">The Corporate Castoff Renaissance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-corporate-castoff-renaissance\" aria-label=\"Link to this section\">#</a></h2>\n<p>Years later, that same brother (yes, redemption arc incoming) was working at a bank that was throwing out perfectly good Dell Optiplex computers. Their only crime? Slow hard drives. These machines were destined for a landfill until he rescued one and brought one to me.</p>\n<p>The transformation was mind-blowing: swapping the old HDD for an SSD made boot time instantaneous. We're talking from \"go make a sandwich while Windows loads\" to \"blink and you'll miss it.\" These corporate castoffs were treasure in disguise!</p>\n<p>I happily used one of these rescued machines for years, only upgrading the video card with something cheap off eBay and naturally the power supply to support it. The best part? Unlike many modern proprietary systems, these old Dells accepted standard parts without too much fuss.</p>\n<h2 id=\"modern-times-from-prebuilt-to-power-user\">Modern Times: From Prebuilt to Power User<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#modern-times-from-prebuilt-to-power-user\" aria-label=\"Link to this section\">#</a></h2>\n<p>Fast forward to last year. I did something that would make younger me gasp in horror—I bought a prebuilt PC from Walmart. I know, I know! But with component prices being what they were, it was actually a good deal.</p>\n<p>Of course, I couldn't leave well enough alone. Soon I was upgrading to a Ryzen 5900X and stuffing it with 4x32GB RAM chips because... reasons? Definitely overkill since I don't even game much anymore. But there's something deeply satisfying about having more RAM than you'll ever need.</p>\n<p>When my fiancée moved in, I wanted us to have matching battle stations so we could work/game together. My brilliant plan: use the old Dell Optiplex case as the foundation for a second build.</p>\n<p>Cue the comedy of errors:</p>\n<ol>\n<li>\n<p>Bought a high-end Radeon 7600XT, only to discover neither of my power supplies could support it.</p>\n</li>\n<li>\n<p>Tried to install the motherboard in the Optiplex case, only to find out it does not support it.</p>\n</li>\n<li>\n<p>Attempted to bench test with my spare PSU, which didn't have the required 8-pin connector.</p>\n</li>\n<li>\n<p>Dismantled my prebuilt setup to test the new motherboard.</p>\n</li>\n<li>\n<p>Got the dreaded amber DRAM light.</p>\n</li>\n<li>\n<p>Finally realized I needed to actually connect a GPU to see anything (rookie mistake).</p>\n</li>\n<li>\n<p>Swapped in the 5900X and... red CPU light. Heart attack moment.</p>\n</li>\n<li>\n<p>Discovered several bent pins on my precious processor.</p>\n</li>\n</ol>\n<p>Ever tried to straighten CPU pins? It's like performing microsurgery with household items. I used a playing card to view each row and a sewing needle for gentle bending. After a significant amount of stress and perhaps some foul language, I managed to somehow, SOMEHOW, fix it without breaking off a single pin. Special thanks to Jayanti for holding the flashlight because I could not see a thing without her.</p>\n<h2 id=\"the-unfinished-symphony\">The Unfinished Symphony<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#the-unfinished-symphony\" aria-label=\"Link to this section\">#</a></h2>\n<p>The second PC isn't completed yet, but it's coming. It'll be a fully custom AM4 build featuring some of the best components it will support. Probably not the wisest financial decision, but since when has PC building been about making sensible choices?</p>\n<p>Looking back at my journey from that hand-me-down 286 to performing pin surgery on high-end AMD processors, I can't help but marvel at how far both technology and my relationship with it have come. From accidentally formatting drives with <code>del *.*</code> to carefully repairing processors that cost more than my first three computers combined, each disaster and triumph has deepened my understanding of these machines.</p>\n<p>My dad's tech obsession became my own, passing down through the generations like some kind of digital inheritance. That rural West Virginia kid with a 286 eventually became someone who could resurrect dead machines and build new ones from scratch. (On a good day)</p>\n<p>Would I recommend this hobby to others? Absolutely! Just maybe don't start by accidentally hosting web servers, formatting your boot drive, or lending your PC to siblings.</p>\n<p>Thank you for joining me on this nostalgic tech journey! Did you experience the hand-me-down computer cycle, DOOM LAN parties, or GeoCities web design? Share your first computer or gaming memories in the comments—I would love to hear how your path through personal computing compares to mine.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "From DOS to Desktop Disasters\nCanonical: https://mnixblog.netlify.app/from-dos-to-desktop-disasters/\nMy Life-Long PC Building Adventure\nI grew up as the youngest offspring of one particularly tech-savvy individual who among many jobs, initially moved our family from many places like Texas, Florida, North Carolina, and eventually to West Virginia where I was raised as a child. The area in particular that I grew up is very rural, and essentially a decade or more behind modern times. However in our household this was almost uncertainly not the case. Since my father was such a connoisseur of technology, it's basically an inherited trait to me. For a little bit of a backstory, we wound up here as my father was writing assembly language code that would help facilitate the operation of coal power plants. So, naturally we always had the latest and greatest trends.\nBecause of the available computing power at my fingertips I was often times able to experiment well before I should have with hardware. In our family, my dad would always upgrade his PC almost annually to the next latest and greatest system. One of my very first computers was a 286 processor with 16mhz. This introduced me to the world of the command line. I learned early how to navigate and traverse folders using Disk Operating System (or DOS). I learned the del *.* command at some point. At least one time or two, this got me into trouble. At home, it wasn't as big of a deal. After deleting DOS from my root directory once or twice, my dad decided to install his own standalone operating system for me that never let me reach the command line, instead I could only pick what game to play and it would type the commands for me.\nThe Tech Hand-Me-Down Dynasty\nOur household had its own tech ecosystem, with my dad at the top of the pyramid. He'd get the cutting-edge system, then his previous machine would go to my oldest brother, whose computer would then go to my middle brother, which meant I'd inherit whatever my middle brother was using. Being the youngest in a tech-obsessed family had its perks, even if I was always three generations behind the curve.\nWhen Dad upgraded to his first Pentium running at a mind-blowing 100MHz, it kicked off a beautiful cascade of hand-me-downs: oldest brother got Dad's 486 DX2 with 66MHz, middle brother received the 486DX with 33MHz, and I finally graduated to the 386 with 33MHz. Each time this happened, it felt like Christmas morning, even though I was essentially getting the technological equivalent of my brothers' worn-out sneakers.\nLiving in a rural West Virginia town where the public schools didn't even connect to the internet until after 2000, our home stood out like a digital oasis in a technological desert. My friends would visit and stare in wonder at our setup. Their jaws would hit the floor when they realized we had dedicated phone lines just for internet—a luxury in the era of dial-up when most families had to choose between making calls or going online.\nDad wasn't just tech-savvy; he ran a dial-up internet service provider. This meant we were always on the cutting edge, while my school was still using computers that might as well have been powered by hamster wheels. I never once experienced the frustration of having someone pick up the phone while I was online—a common trauma for most 90s kids.\nDOOM, LAN Parties, and the Cruel Reality of Crashed Map Editors\nThe mid-90s were the golden age of DOOM, and our basement became command central for epic LAN parties around 1995. Back then, Windows couldn't hold a candle to DOS when it came to gaming performance. We would gather, connect our machines, and blast demons (or each other in deathmatch) for hours on end.\nBut we weren't just players—we were creators too. The DOOM modding scene was where I cut my teeth on level design, spending countless hours crafting custom maps... only to have my underpowered 386 crash mid-creation, erasing everything I'd done. I can still feel that particular brand of heartbreak that comes from losing hours of creative work to the digital void. Nothing teaches you to save your work like losing an entire afternoon's worth of meticulously placed monsters and secret rooms.\nStorage in our house was a sight to behold. We literally had hundreds of 5.25\" floppy disks stashed everywhere—loaded with games, programming languages, and applications from computing's early days. These massive, actually-floppy disks are probably still boxed up somewhere in my parents' house, a physical museum of software history.\nI still remember the technological disappointment of our first 1x CD-ROM drive. On paper, it could hold way more data than any floppy. In practice? I discovered firsthand that it was often faster to install Windows using 64 separate 1.44\" floppy disks than waiting for that sluggish CD to do its thing. The only advantage was being able to start the installation and walk away instead of sitting there feeding in disk after disk like some sort of human vending machine.\nThe Windows Product Key Savant\nHere's a weird party trick I still have: I can recite the Windows 95 and 98 product keys from memory. Seriously. After installing these operating systems dozens of times (usually after breaking something), those alphanumeric sequences are permanently etched into my brain alongside my social security number and childhood phone number.\nWas it strictly kosher to use the same key repeatedly? Probably not. But in those days, activation was simpler, and we were just kids excited to get our machines back up and running after whatever catastrophe had befallen them (usually self-inflicted).\nAccidental Webmaster and the Great Apache Incident\nWhile most kids were content playing Nintendo, my brothers and I were diving headfirst into the wild west of early web development. We honed our skills on Geocities and AngelFire, painstakingly writing HTML in Notepad and proudly publishing pages that were essentially 99% animated GIFs and \"Under Construction\" banners.\nWhen Dad discovered our interest, he kicked things up several notches—purchasing us a domain name, setting up a web server, and giving each of us our own subdomain. Suddenly we graduated from Notepad to the \"professional-grade\" Microsoft FrontPage and were learning about mysterious concepts like FTP and hosting. These were the early days of web development. JavaScript was not nearly as powerful as it has become.\nOne of my more notorious tech moments came when I accidentally installed Apache on my PC and started hosting a web server without even realizing what I'd done. Dad went into full-on security panic mode, convinced we'd been hacked, only to discover it was just his clueless youngest son experimenting with software he didn't understand. When he demanded to know why I'd done it, I genuinely had no idea what he was talking about—I didn't even know what Apache was! I'd just clicked through some install prompts and suddenly our home network had an unexpected web server. The digital equivalent of accidentally building a shed in the backyard.\nEmulation: Console Gaming on PC Terms\nWhile my friends were saving up allowance money for GameBoy cartridges and Super Nintendo games, we were exploring the fascinating world of emulation. Why play Super Mario on a tiny GameBoy screen when you could play it on a computer monitor with the ability to save anywhere and apply cheats without those clunky GameShark devices?\nIt wasn't that we didn't have consoles—our gaming collection included everything from ancient Ataris and Commodore 64s to Nintendo, Super Nintendo, Genesis, Sega CD, and N64. But the flexibility of emulation gave us superpowers that console gamers could only dream of.\nThe crown jewel of our portable gaming collection wasn't even a Nintendo product—it was the Sega Nomad, a handheld that played full Genesis cartridges with a backlit screen when that was basically unheard of technology. This thing was truly revolutionary—a fully backlit portable console playing 16-bit games when the GameBoy was still rocking monochrome graphics and requiring external light sources.\nBreaking Free: My First Custom Build\nFast forward to my teenage years. I was finally ready to break free from the hand-me-down cycle and build my very first PC from scratch. Spoiler alert: it didn't go smoothly.\nMy first motherboard purchase ended with the vendor vanishing faster than my motivation to do homework. Still salty about that one, but it taught me an early lesson about researching vendors before handing over cash.\nEventually, I assembled what felt like a supercomputer at the time: a Pentium 4 Dual Core running at 1.2GHz paired with some NVIDIA graphics card whose model number has long since faded from memory. But it ran World of Warcraft on maximum settings, and that's all that mattered to teenage me.\nThe Brother \"Borrowing\" Incident\nWhen I moved out west, my computer stayed behind. At some point, my brother decided my PC looked lonely and \"borrowed\" it without asking (as brothers do). Then Christmas rolled around, and guess what appeared under the tree with my name on it? MY OWN PC. With a blown power supply.\nI never did replace that power supply. The PC sat gathering dust, a monument to sibling shenanigans and the importance of surge protectors.\nThe Corporate Castoff Renaissance\nYears later, that same brother (yes, redemption arc incoming) was working at a bank that was throwing out perfectly good Dell Optiplex computers. Their only crime? Slow hard drives. These machines were destined for a landfill until he rescued one and brought one to me.\nThe transformation was mind-blowing: swapping the old HDD for an SSD made boot time instantaneous. We're talking from \"go make a sandwich while Windows loads\" to \"blink and you'll miss it.\" These corporate castoffs were treasure in disguise!\nI happily used one of these rescued machines for years, only upgrading the video card with something cheap off eBay and naturally the power supply to support it. The best part? Unlike many modern proprietary systems, these old Dells accepted standard parts without too much fuss.\nModern Times: From Prebuilt to Power User\nFast forward to last year. I did something that would make younger me gasp in horror—I bought a prebuilt PC from Walmart. I know, I know! But with component prices being what they were, it was actually a good deal.\nOf course, I couldn't leave well enough alone. Soon I was upgrading to a Ryzen 5900X and stuffing it with 4x32GB RAM chips because... reasons? Definitely overkill since I don't even game much anymore. But there's something deeply satisfying about having more RAM than you'll ever need.\nWhen my fiancée moved in, I wanted us to have matching battle stations so we could work/game together. My brilliant plan: use the old Dell Optiplex case as the foundation for a second build.\nCue the comedy of errors:\nBought a high-end Radeon 7600XT, only to discover neither of my power supplies could support it.\nTried to install the motherboard in the Optiplex case, only to find out it does not support it.\nAttempted to bench test with my spare PSU, which didn't have the required 8-pin connector.\nDismantled my prebuilt setup to test the new motherboard.\nGot the dreaded amber DRAM light.\nFinally realized I needed to actually connect a GPU to see anything (rookie mistake).\nSwapped in the 5900X and... red CPU light. Heart attack moment.\nDiscovered several bent pins on my precious processor.\nEver tried to straighten CPU pins? It's like performing microsurgery with household items. I used a playing card to view each row and a sewing needle for gentle bending. After a significant amount of stress and perhaps some foul language, I managed to somehow, SOMEHOW, fix it without breaking off a single pin. Special thanks to Jayanti for holding the flashlight because I could not see a thing without her.\nThe Unfinished Symphony\nThe second PC isn't completed yet, but it's coming. It'll be a fully custom AM4 build featuring some of the best components it will support. Probably not the wisest financial decision, but since when has PC building been about making sensible choices?\nLooking back at my journey from that hand-me-down 286 to performing pin surgery on high-end AMD processors, I can't help but marvel at how far both technology and my relationship with it have come. From accidentally formatting drives with del *.* to carefully repairing processors that cost more than my first three computers combined, each disaster and triumph has deepened my understanding of these machines.\nMy dad's tech obsession became my own, passing down through the generations like some kind of digital inheritance. That rural West Virginia kid with a 286 eventually became someone who could resurrect dead machines and build new ones from scratch. (On a good day)\nWould I recommend this hobby to others? Absolutely! Just maybe don't start by accidentally hosting web servers, formatting your boot drive, or lending your PC to siblings.\nThank you for joining me on this nostalgic tech journey! Did you experience the hand-me-down computer cycle, DOOM LAN parties, or GeoCities web design? Share your first computer or gaming memories in the comments—I would love to hear how your path through personal computing compares to mine.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/from-dos-to-desktop-disasters/#ob-discussion)",
      "date_published": "2025-04-28T04:31:56Z",
      "id": "https://mnixblog.netlify.app/from-dos-to-desktop-disasters/",
      "image": "https://mnixblog.netlify.app/images/from-dos-to-desktop-disasters/cover.jpg",
      "summary": "Growing up on my dad's hand-me-down PCs: a 286 and DOS, DOOM LAN parties, the Great Apache Incident, and my first custom build.",
      "tags": [
        "Career"
      ],
      "title": "From DOS to Desktop Disasters",
      "url": "https://mnixblog.netlify.app/from-dos-to-desktop-disasters/"
    },
    {
      "content_html": "<p>Which Web Component features can you use today, and which are still ideas? This post sorts them, with a status table you can check against your own browser.</p>\n<p>Some features are ready now, some work in only some browsers, and a few are only proposals. Knowing which is which keeps us from building on something that does not exist.</p>\n<p><strong>Prerequisites in two lines:</strong> a custom element is a JavaScript class registered with <code>customElements.define()</code>, and a shadow root gives it private markup and styles. Everything below extends those two ideas.</p>\n<h2 id=\"what-ships-today-and-what-does-not\">What ships today and what does not<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#what-ships-today-and-what-does-not\" aria-label=\"Link to this section\">#</a></h2>\n<p>\"Works\" in the second column means a small script ran in headless Chrome 156 while editing this post. Firefox and Safari were not tested by us. The Status column was checked against <a href=\"https://webstatus.dev/\">webstatus.dev</a> and MDN on 26 October 2026, so treat it as a snapshot and look again before you rely on a row.</p>\n<table><thead><tr><th>Feature</th><th>In Chrome 156</th><th>Status, Oct 2026</th></tr></thead><tbody>\n<tr><td><code>::part()</code>: style what a component exposes</td><td>Works</td><td>Ships today</td></tr>\n<tr><td><code>:state()</code>: style a component by its own state</td><td>Works</td><td>Baseline newly available</td></tr>\n<tr><td>Declarative Shadow DOM: <code>shadowrootmode</code></td><td>Works</td><td>Baseline widely available</td></tr>\n<tr><td>Manual slot assignment: <code>slot.assign()</code></td><td>Works</td><td>Baseline widely available</td></tr>\n<tr><td><code>@scope</code>: limit where a CSS rule applies</td><td>Works</td><td>Baseline newly available</td></tr>\n<tr><td>Scoped registries</td><td>Works</td><td>Chrome, Edge, Safari only</td></tr>\n<tr><td><code>attachSlot()</code></td><td><code>undefined</code></td><td>Not an API</td></tr>\n<tr><td><code>accessibleRole</code> property</td><td>Not present</td><td>Not an API</td></tr>\n<tr><td>Declarative custom elements</td><td>Not testable</td><td>Proposal</td></tr>\n</tbody></table>\n<p>The dates behind the Status column, from webstatus.dev and MDN on 26 October 2026:</p>\n<ul>\n<li><code>:state()</code>: newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4).</li>\n<li>Declarative Shadow DOM: newly available in February 2024 (Chrome 111, Firefox 123, Safari 16.4) and widely available since August 2026. The older <code>shadowroot</code> attribute no longer works.</li>\n<li>Manual slot assignment: widely available in all three engines since March 2023.</li>\n<li><code>@scope</code>: newly available since March 2026.</li>\n<li>Scoped registries: Chrome and Edge 146, Safari 26. Firefox has not shipped it. It is in Nightly, and Mozilla has proposed shipping it in Firefox 159, so check before you rely on it.</li>\n<li><code>::part()</code> was not re-checked for this update.</li>\n</ul>\n<h2 id=\"the-rising-tide-emerging-trends\">The Rising Tide: Emerging Trends<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#the-rising-tide-emerging-trends\" aria-label=\"Link to this section\">#</a></h2>\n<p>The rest of this post covers libraries, state and a few patterns. Most of it is built on the standards above, not added to the browser, and none of it is needed to write a custom element.</p>\n<h3 id=\"web-component-libraries\">Web Component Libraries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#web-component-libraries\" aria-label=\"Link to this section\">#</a></h3>\n<p>You do not need a library to write a custom element, and every example above works without one. A library saves boilerplate for the parts you end up writing again and again.</p>\n<p><strong>Lit</strong>, from Google, is a small base class that adds reactive properties and templates on top of custom elements. <strong>Stencil</strong> is a compiler: you write TypeScript and JSX and it produces standards-based custom elements. <strong>FAST</strong> and <strong>Shoelace</strong> are the other two names that come up most. Each project changes its name, its version and its advice over time, so check its own site for its current state before you pick one. We did not re-check them for this update.</p>\n<p>The cost is the usual one. A library is one more thing to learn and to keep up to date, and shadow DOM still makes global styling harder whichever one you choose.</p>\n<h3 id=\"is-state-management-going-native-beyond-properties-and-attributes\">Is State Management Going Native?: Beyond Properties and Attributes<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#is-state-management-going-native-beyond-properties-and-attributes\" aria-label=\"Link to this section\">#</a></h3>\n<p>The browser gives you less state management than a framework does, and that is fine. The part it does give you is <code>:state()</code>: a component can expose states such as \"loading\" or \"expanded\" to CSS without adding classes the page could overwrite. It has been Baseline newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4). Stores, signals and reducers are libraries and patterns, not platform features, and the examples below are patterns you can write yourself.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">State Management in Web Components</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;user-profile&gt;&lt;/user-profile&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n        margin: 0;\n        padding: 20px;\n        font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n        min-height: 100vh;\n        transition: background-color 0.3s ease, color 0.3s ease;\n    }\n\n    @media (prefers-color-scheme: dark) {\n        body {\n            background-color: #121212;\n            color: #f0f0f0;\n        }\n    }\n\n    @media (prefers-color-scheme: light) {\n        body {\n            background-color: #f8f9fa;\n            color: #333;\n        }\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Simple state management pattern for Web Components\nclass UserProfile extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initialize state\n    this.state = {\n      name: &#39;Guest User&#39;,\n      role: &#39;Visitor&#39;,\n      isLoggedIn: false,\n      darkMode: window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches };\n\n\n    // Listen for changes in color scheme preference\n    this.colorSchemeMedia = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;);\n    this.colorSchemeMedia.addEventListener(&#39;change&#39;, e =&gt; {\n      this.setState({ darkMode: e.matches });\n    });\n\n    this.render();\n  }\n\n  // State management method\n  setState(newState) {\n    this.state = { ...this.state, ...newState };\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: system-ui, sans-serif;\n          max-width: 300px;\n          padding: 20px;\n          border: 1px solid ${this.state.darkMode ? &#39;#555&#39; : &#39;#ddd&#39;};\n          border-radius: 8px;\n          background-color: ${this.state.darkMode ? &#39;#222&#39; : &#39;#fff&#39;};\n          color: ${this.state.darkMode ? &#39;#eee&#39; : &#39;#333&#39;};\n        }\n        \n        .profile {\n          display: flex;\n          flex-direction: column;\n          align-items: center;\n        }\n        \n        .avatar {\n          width: 80px;\n          height: 80px;\n          border-radius: 50%;\n          background-color: ${this.state.isLoggedIn ? &#39;#4CAF50&#39; : this.state.darkMode ? &#39;#555&#39; : &#39;#ccc&#39;};\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          margin-bottom: 16px;\n          color: white;\n          font-size: 32px;\n        }\n        \n        .name {\n          font-size: 20px;\n          font-weight: bold;\n          margin-bottom: 4px;\n        }\n        \n        .role {\n          color: ${this.state.darkMode ? &#39;#aaa&#39; : &#39;#666&#39;};\n          margin-bottom: 16px;\n        }\n        \n        button {\n          background-color: ${this.state.isLoggedIn ? &#39;#f44336&#39; : &#39;#4CAF50&#39;};\n          color: white;\n          border: none;\n          padding: 8px 16px;\n          border-radius: 4px;\n          cursor: pointer;\n        }\n        \n        button:hover {\n          opacity: 0.9;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;profile&quot;&gt;\n        &lt;div class=&quot;avatar&quot;&gt;${this.state.name.charAt(0)}&lt;/div&gt;\n        &lt;div class=&quot;name&quot;&gt;${this.state.name}&lt;/div&gt;\n        &lt;div class=&quot;role&quot;&gt;${this.state.role}&lt;/div&gt;\n        &lt;button id=&quot;toggle-login&quot;&gt;\n          ${this.state.isLoggedIn ? &#39;Logout&#39; : &#39;Login&#39;}\n        &lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;#toggle-login&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      if (this.state.isLoggedIn) {\n        this.setState({\n          name: &#39;Guest User&#39;,\n          role: &#39;Visitor&#39;,\n          isLoggedIn: false });\n\n      } else {\n        this.setState({\n          name: &#39;Jane Doe&#39;,\n          role: &#39;Administrator&#39;,\n          isLoggedIn: true });\n\n      }\n    });\n  }\n\n  disconnectedCallback() {\n    // Clean up event listener when component is removed\n    if (this.colorSchemeMedia) {\n      this.colorSchemeMedia.removeEventListener(&#39;change&#39;, this.handleColorSchemeChange);\n    }\n  }}\n\n\ncustomElements.define(&#39;user-profile&#39;, UserProfile);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"context-sharing-state-without-tight-coupling\">Context: Sharing State Without Tight Coupling<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#context-sharing-state-without-tight-coupling\" aria-label=\"Link to this section\">#</a></h3>\n<p>A context-style provider is a pattern, not a platform feature. A parent component holds a value, and the components inside it ask for that value by dispatching an event, so the provider and its consumers do not need to know about each other. A <code>ThemeProvider</code> is the usual example, and the one below is small enough to read in a minute. For plain style values, CSS custom properties are still simpler.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Advanced State Management with Context</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;theme-provider&gt;\n\t&lt;theme-consumer&gt;&lt;/theme-consumer&gt;\n\t&lt;theme-toggler&gt;&lt;/theme-toggler&gt;\n\t&lt;theme-controller&gt;&lt;/theme-controller&gt;\n&lt;/theme-provider&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Context-like pattern for Web Components\nclass ThemeContext {\n  constructor() {\n    // Check for user preference\n    const prefersDarkMode =\n    window.matchMedia &amp;&amp;\n    window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;\n\n    this.theme = prefersDarkMode ? &quot;dark&quot; : &quot;light&quot;;\n    this.accentColor = &quot;#2196F3&quot;;\n    this.fontSize = &quot;medium&quot;;\n    this.borderRadius = &quot;4px&quot;;\n    this.listeners = new Set();\n\n    // Track the last changed setting to help with animations\n    this.lastChanged = null;\n\n    // Listen for system theme changes\n    this.mediaQuery = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;);\n    this.handleSystemThemeChange = e =&gt; {\n      this.setTheme(e.matches ? &quot;dark&quot; : &quot;light&quot;);\n    };\n\n    // Add listener for system theme changes if supported\n    if (this.mediaQuery.addEventListener) {\n      this.mediaQuery.addEventListener(&quot;change&quot;, this.handleSystemThemeChange);\n    }\n\n    // Apply initial theme to body/html/root\n    this.applyThemeToDocument(this.theme);\n  }\n\n  getTheme() {\n    return this.theme;\n  }\n\n  setTheme(newTheme) {\n    this.theme = newTheme;\n    this.lastChanged = &quot;theme&quot;;\n    this.applyThemeToDocument(newTheme);\n    this.notifyListeners();\n  }\n\n  applyThemeToDocument(theme) {\n    // Set theme on html and body elements\n    document.documentElement.style.backgroundColor =\n    theme === &quot;light&quot; ? &quot;#f5f5f5&quot; : &quot;#121212&quot;;\n    document.documentElement.style.transition = &quot;background-color 0.3s&quot;;\n\n    document.body.style.backgroundColor =\n    theme === &quot;light&quot; ? &quot;#f5f5f5&quot; : &quot;#121212&quot;;\n    document.body.style.color = theme === &quot;light&quot; ? &quot;#333333&quot; : &quot;#ffffff&quot;;\n    document.body.style.transition = &quot;background-color 0.3s, color 0.3s&quot;;\n\n    // Add/remove class on document root for additional styling hooks\n    if (theme === &quot;dark&quot;) {\n      document.documentElement.classList.add(&quot;dark-theme&quot;);\n    } else {\n      document.documentElement.classList.remove(&quot;dark-theme&quot;);\n    }\n  }\n\n  getAccentColor() {\n    return this.accentColor;\n  }\n\n  setAccentColor(newColor) {\n    this.accentColor = newColor;\n    this.lastChanged = &quot;accentColor&quot;;\n    this.notifyListeners();\n  }\n\n  getFontSize() {\n    return this.fontSize;\n  }\n\n  setFontSize(newSize) {\n    this.fontSize = newSize;\n    this.lastChanged = &quot;fontSize&quot;;\n    this.notifyListeners();\n  }\n\n  getBorderRadius() {\n    return this.borderRadius;\n  }\n\n  setBorderRadius(newRadius) {\n    this.borderRadius = newRadius;\n    this.lastChanged = &quot;borderRadius&quot;;\n    this.notifyListeners();\n  }\n\n  getLastChanged() {\n    return this.lastChanged;\n  }\n\n  // Get the current theme as a complete object\n  getCurrentThemeSettings() {\n    return {\n      theme: this.theme,\n      accentColor: this.accentColor,\n      fontSize: this.fontSize,\n      borderRadius: this.borderRadius };\n\n  }\n\n  subscribe(listener) {\n    this.listeners.add(listener);\n  }\n\n  unsubscribe(listener) {\n    this.listeners.delete(listener);\n  }\n\n  notifyListeners() {\n    this.listeners.forEach((listener) =&gt;\n    listener(this.getCurrentThemeSettings()));\n\n  }\n\n  disconnectMediaQuery() {\n    // Clean up media query listener if it exists\n    if (this.mediaQuery &amp;&amp; this.mediaQuery.removeEventListener) {\n      this.mediaQuery.removeEventListener(&quot;change&quot;, this.handleSystemThemeChange);\n    }\n  }}\n\n\n// Global context instance\nconst themeContext = new ThemeContext();\n\n// Add window unload handler to clean up listeners\nwindow.addEventListener(&quot;unload&quot;, () =&gt; {\n  themeContext.disconnectMediaQuery();\n});\n\n// Provider component\nclass ThemeProvider extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: system-ui, sans-serif;\n          padding: 20px;\n          background-color: ${\n    themeContext.getTheme() === &quot;light&quot; ? &quot;#ffffff&quot; : &quot;#333333&quot;\n    };\n          color: ${themeContext.getTheme() === &quot;light&quot; ? &quot;#333333&quot; : &quot;#ffffff&quot;};\n          border-radius: 8px;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .container {\n          max-width: 600px;\n          margin: 0 auto;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;container&quot;&gt;\n        &lt;h2&gt;Theme Context Demo&lt;/h2&gt;\n        &lt;slot&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n\n    // Update when theme changes\n    this.updateTheme = settings =&gt; {\n      const theme = settings.theme;\n      const styleElement = this.shadowRoot.querySelector(&quot;style&quot;);\n\n      // Update the style element with new theme colors\n      styleElement.textContent = `\n        :host {\n          display: block;\n          font-family: system-ui, sans-serif;\n          padding: 20px;\n          background-color: ${theme === &quot;light&quot; ? &quot;#ffffff&quot; : &quot;#333333&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333333&quot; : &quot;#ffffff&quot;};\n          border-radius: 8px;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .container {\n          max-width: 600px;\n          margin: 0 auto;\n        }\n      `;\n    };\n\n    themeContext.subscribe(this.updateTheme);\n  }\n\n  disconnectedCallback() {\n    themeContext.unsubscribe(this.updateTheme);\n  }}\n\n\n// Consumer component\nclass ThemeConsumer extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this.render();\n\n    // Update when theme changes\n    this.updateTheme = settings =&gt; {\n      // Store which value changed to highlight it for the user\n      this.lastChanged = themeContext.getLastChanged();\n      this.render();\n\n      // Reset highlight after animation completes\n      setTimeout(() =&gt; {\n        this.lastChanged = null;\n        this.render();\n      }, 1500);\n    };\n\n    themeContext.subscribe(this.updateTheme);\n  }\n\n  disconnectedCallback() {\n    themeContext.unsubscribe(this.updateTheme);\n  }\n\n  render() {\n    const settings = themeContext.getCurrentThemeSettings();\n    const theme = settings.theme;\n    const accentColor = settings.accentColor;\n    const fontSize = settings.fontSize;\n    const borderRadius = settings.borderRadius;\n    const lastChanged = this.lastChanged;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 16px 0;\n          padding: 16px;\n          border-radius: ${borderRadius};\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#f9f9f9&quot; : &quot;#444&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#eee&quot;};\n          transition: all 0.3s;\n        }\n        \n        h3 {\n          margin-top: 0;\n          margin-bottom: 12px;\n          color: ${accentColor};\n          font-size: ${\n    fontSize === &quot;small&quot; ?\n    &quot;16px&quot; :\n    fontSize === &quot;medium&quot; ?\n    &quot;18px&quot; :\n    &quot;20px&quot;\n    };\n          transition: all 0.3s;\n        }\n        \n        .theme-properties {\n          display: grid;\n          grid-template-columns: 120px 1fr;\n          grid-gap: 8px;\n        }\n        \n        .property-name {\n          font-weight: bold;\n          opacity: 0.8;\n        }\n        \n        .property-value {\n          font-family: monospace;\n          padding: 2px 6px;\n          border-radius: ${borderRadius};\n          background-color: ${\n    theme === &quot;light&quot; ? &quot;rgba(0,0,0,0.05)&quot; : &quot;rgba(255,255,255,0.1)&quot;\n    };\n          transition: all 0.3s;\n        }\n        \n        .property-highlight {\n          position: relative;\n          animation: pulse 1.5s ease-in-out;\n        }\n        \n        .property-highlight::before {\n          content: &quot;&quot;;\n          position: absolute;\n          top: 0;\n          right: 0;\n          bottom: 0;\n          left: 0;\n          background-color: ${accentColor};\n          opacity: 0.2;\n          border-radius: ${borderRadius};\n          z-index: -1;\n        }\n        \n        .color-preview {\n          display: inline-block;\n          width: 14px;\n          height: 14px;\n          border-radius: 50%;\n          margin-right: 6px;\n          vertical-align: middle;\n        }\n        \n        .demo-text {\n          margin-top: 16px;\n          padding: 12px;\n          border-radius: ${borderRadius};\n          border-left: 4px solid ${accentColor};\n          font-size: ${\n    fontSize === &quot;small&quot; ?\n    &quot;14px&quot; :\n    fontSize === &quot;medium&quot; ?\n    &quot;16px&quot; :\n    &quot;18px&quot;\n    };\n          background-color: ${\n    theme === &quot;light&quot; ? &quot;rgba(0,0,0,0.02)&quot; : &quot;rgba(255,255,255,0.05)&quot;\n    };\n          transition: all 0.3s;\n        }\n        \n        @keyframes pulse {\n          0% { transform: scale(1); }\n          50% { transform: scale(1.03); }\n          100% { transform: scale(1); }\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;h3&gt;Theme State Observer&lt;/h3&gt;\n        \n        &lt;div class=&quot;theme-properties&quot;&gt;\n          &lt;span class=&quot;property-name&quot;&gt;Theme:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;theme&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            ${theme}\n          &lt;/span&gt;\n          \n          &lt;span class=&quot;property-name&quot;&gt;Accent Color:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;accentColor&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            &lt;span class=&quot;color-preview&quot; style=&quot;background-color: ${accentColor};&quot;&gt;&lt;/span&gt;\n            ${accentColor}\n          &lt;/span&gt;\n          \n          &lt;span class=&quot;property-name&quot;&gt;Font Size:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;fontSize&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            ${fontSize}\n          &lt;/span&gt;\n          \n          &lt;span class=&quot;property-name&quot;&gt;Border Radius:&lt;/span&gt;\n          &lt;span class=&quot;property-value ${\n    lastChanged === &quot;borderRadius&quot; ? &quot;property-highlight&quot; : &quot;&quot;\n    }&quot;&gt;\n            ${borderRadius}\n          &lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;demo-text&quot;&gt;\n          This text demonstrates the current theme settings. The border radius, font size,\n          and colors are all controlled by the theme context.\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Theme toggler component\nclass ThemeToggler extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Create bound event handler to avoid recreating it\n    this.handleToggle = this.handleToggle.bind(this);\n\n    // Subscribe to theme changes\n    this.updateTheme = () =&gt; {\n      this.render();\n    };\n\n    themeContext.subscribe(this.updateTheme);\n    this.render();\n  }\n\n  disconnectedCallback() {\n    // Clean up event listeners and subscriptions\n    themeContext.unsubscribe(this.updateTheme);\n    this.removeToggleListener();\n  }\n\n  removeToggleListener() {\n    const button = this.shadowRoot.querySelector(&quot;#theme-toggle&quot;);\n    if (button) {\n      button.removeEventListener(&quot;click&quot;, this.handleToggle);\n    }\n  }\n\n  handleToggle() {\n    const currentTheme = themeContext.getTheme();\n    themeContext.setTheme(currentTheme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;);\n  }\n\n  render() {\n    // Clean up previous event listeners before re-rendering\n    this.removeToggleListener();\n\n    const theme = themeContext.getTheme();\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 16px 0;\n          padding: 16px;\n          border-radius: 4px;\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#f9f9f9&quot; : &quot;#444&quot;};\n          transition: background-color 0.3s, border-color 0.3s;\n        }\n        \n        button {\n          background-color: ${theme === &quot;light&quot; ? &quot;#4CAF50&quot; : &quot;#66BB6A&quot;};\n          color: white;\n          border: none;\n          padding: 8px 16px;\n          border-radius: 4px;\n          cursor: pointer;\n          font-size: 16px;\n          transition: background-color 0.3s;\n        }\n        \n        button:hover {\n          background-color: ${theme === &quot;light&quot; ? &quot;#3e8e41&quot; : &quot;#4CAF50&quot;};\n          box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;p&gt;Click the button to toggle the theme:&lt;/p&gt;\n        &lt;button id=&quot;theme-toggle&quot;&gt;\n          Switch to ${theme === &quot;light&quot; ? &quot;Dark&quot; : &quot;Light&quot;} Theme\n        &lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listener\n    this.shadowRoot.\n    querySelector(&quot;#theme-toggle&quot;).\n    addEventListener(&quot;click&quot;, this.handleToggle);\n  }}\n\n\n// Theme controller component\nclass ThemeController extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Get current theme settings\n    this.theme = themeContext.getTheme();\n    this.accentColor = themeContext.getAccentColor();\n    this.fontSize = themeContext.getFontSize();\n    this.borderRadius = themeContext.getBorderRadius();\n\n    // Define preset accent colors\n    this.accentColors = [\n    &quot;#2196F3&quot;, // Blue (default)\n    &quot;#F44336&quot;, // Red\n    &quot;#4CAF50&quot;, // Green\n    &quot;#FF9800&quot;, // Orange\n    &quot;#9C27B0&quot;, // Purple\n    &quot;#00BCD4&quot;, // Cyan\n    &quot;#607D8B&quot; // Blue Grey\n    ];\n\n    // Define preset font sizes\n    this.fontSizes = [&quot;small&quot;, &quot;medium&quot;, &quot;large&quot;];\n\n    // Define preset border radius values\n    this.borderRadiusOptions = [&quot;0px&quot;, &quot;4px&quot;, &quot;8px&quot;, &quot;12px&quot;, &quot;16px&quot;];\n\n    this.render();\n\n    // Update when theme changes\n    this.updateTheme = settings =&gt; {\n      this.theme = settings.theme;\n      this.accentColor = settings.accentColor;\n      this.fontSize = settings.fontSize;\n      this.borderRadius = settings.borderRadius;\n      this.render();\n    };\n\n    themeContext.subscribe(this.updateTheme);\n  }\n\n  disconnectedCallback() {\n    themeContext.unsubscribe(this.updateTheme);\n  }\n\n  addEventListeners() {\n    // Theme toggle\n    const themeToggle = this.shadowRoot.getElementById(&quot;theme-toggle&quot;);\n    themeToggle.addEventListener(&quot;click&quot;, () =&gt; {\n      themeContext.setTheme(this.theme === &quot;light&quot; ? &quot;dark&quot; : &quot;light&quot;);\n    });\n\n    // Accent color selection\n    const colorButtons = this.shadowRoot.querySelectorAll(&quot;.color-option&quot;);\n    colorButtons.forEach(button =&gt; {\n      button.addEventListener(&quot;click&quot;, () =&gt; {\n        const color = button.getAttribute(&quot;data-color&quot;);\n        themeContext.setAccentColor(color);\n      });\n    });\n\n    // Font size selection\n    const fontSizeSelect = this.shadowRoot.getElementById(&quot;font-size-select&quot;);\n    fontSizeSelect.value = this.fontSize;\n    fontSizeSelect.addEventListener(&quot;change&quot;, () =&gt; {\n      themeContext.setFontSize(fontSizeSelect.value);\n    });\n\n    // Border radius slider\n    const radiusSlider = this.shadowRoot.getElementById(&quot;radius-slider&quot;);\n    const radiusValue = this.shadowRoot.getElementById(&quot;radius-value&quot;);\n\n    // Set initial value\n    const currentRadiusIndex = this.borderRadiusOptions.indexOf(\n    this.borderRadius);\n\n    radiusSlider.value = currentRadiusIndex &gt;= 0 ? currentRadiusIndex : 1;\n    radiusValue.textContent = this.borderRadius;\n\n    radiusSlider.addEventListener(&quot;input&quot;, () =&gt; {\n      const newRadius = this.borderRadiusOptions[radiusSlider.value];\n      radiusValue.textContent = newRadius;\n      themeContext.setBorderRadius(newRadius);\n    });\n  }\n\n  render() {\n    const theme = this.theme;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 16px 0;\n          padding: 16px;\n          border-radius: ${this.borderRadius};\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#f9f9f9&quot; : &quot;#444&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#eee&quot;};\n          transition: all 0.3s;\n          font-family: system-ui, sans-serif;\n        }\n        \n        h3 {\n          margin-top: 0;\n          color: ${this.accentColor};\n          transition: color 0.3s;\n        }\n        \n        .control-group {\n          margin-bottom: 16px;\n        }\n        \n        .control-group label {\n          display: block;\n          margin-bottom: 8px;\n          font-weight: bold;\n        }\n        \n        .theme-toggle {\n          cursor: pointer;\n          background-color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#f5f5f5&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#fff&quot; : &quot;#333&quot;};\n          border: none;\n          border-radius: ${this.borderRadius};\n          padding: 8px 16px;\n          font-size: 14px;\n          display: flex;\n          align-items: center;\n          justify-content: space-between;\n          width: 140px;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .theme-toggle .icon {\n          display: inline-block;\n          width: 16px;\n          height: 16px;\n          border-radius: 50%;\n          background-color: ${theme === &quot;light&quot; ? &quot;#f5f5f5&quot; : &quot;#333&quot;};\n          margin-left: 8px;\n          position: relative;\n        }\n        \n        .theme-toggle .icon::before {\n          content: &#39;&#39;;\n          position: absolute;\n          top: 3px;\n          left: ${theme === &quot;light&quot; ? &quot;3px&quot; : &quot;12px&quot;};\n          width: 10px;\n          height: 10px;\n          border-radius: 50%;\n          background-color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#f5f5f5&quot;};\n          transition: left 0.3s;\n        }\n        \n        .color-options {\n          display: flex;\n          gap: 8px;\n          flex-wrap: wrap;\n        }\n        \n        .color-option {\n          width: 32px;\n          height: 32px;\n          border-radius: 50%;\n          cursor: pointer;\n          position: relative;\n          border: 2px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          transition: transform 0.2s;\n        }\n        \n        .color-option:hover {\n          transform: scale(1.1);\n        }\n        \n        .color-option.active {\n          border: 2px solid ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#fff&quot;};\n        }\n        \n        .color-option.active::after {\n          content: &quot;✓&quot;;\n          position: absolute;\n          top: 50%;\n          left: 50%;\n          transform: translate(-50%, -50%);\n          color: ${this.isDarkColor(this.accentColor) ? &quot;#fff&quot; : &quot;#333&quot;};\n          font-size: 18px;\n          font-weight: bold;\n        }\n        \n        select, input[type=&quot;range&quot;] {\n          width: 100%;\n          padding: 6px;\n          border-radius: ${this.borderRadius};\n          border: 1px solid ${theme === &quot;light&quot; ? &quot;#ddd&quot; : &quot;#555&quot;};\n          background-color: ${theme === &quot;light&quot; ? &quot;#fff&quot; : &quot;#333&quot;};\n          color: ${theme === &quot;light&quot; ? &quot;#333&quot; : &quot;#fff&quot;};\n        }\n        \n        .slider-container {\n          display: flex;\n          align-items: center;\n          gap: 12px;\n        }\n        \n        .slider-container input {\n          flex: 1;\n        }\n        \n        .value-display {\n          min-width: 40px;\n          text-align: center;\n          font-family: monospace;\n          padding: 4px 8px;\n          background-color: ${\n    theme === &quot;light&quot; ? &quot;rgba(0,0,0,0.05)&quot; : &quot;rgba(255,255,255,0.1)&quot;\n    };\n          border-radius: ${this.borderRadius};\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;h3&gt;Theme Controller&lt;/h3&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Theme Mode&lt;/label&gt;\n          &lt;button id=&quot;theme-toggle&quot; class=&quot;theme-toggle&quot;&gt;\n            ${theme === &quot;light&quot; ? &quot;Light Mode&quot; : &quot;Dark Mode&quot;}\n            &lt;span class=&quot;icon&quot;&gt;&lt;/span&gt;\n          &lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Accent Color&lt;/label&gt;\n          &lt;div class=&quot;color-options&quot;&gt;\n            ${this.accentColors.\n    map(\n    color =&gt; `\n              &lt;div \n                class=&quot;color-option ${\n    color === this.accentColor ? &quot;active&quot; : &quot;&quot;\n    }&quot; \n                style=&quot;background-color: ${color};&quot;\n                data-color=&quot;${color}&quot;&gt;\n              &lt;/div&gt;\n            `).\n\n    join(&quot;&quot;)}\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Font Size&lt;/label&gt;\n          &lt;select id=&quot;font-size-select&quot;&gt;\n            ${this.fontSizes.\n    map(\n    size =&gt; `\n              &lt;option value=&quot;${size}&quot; ${\n    this.fontSize === size ? &quot;selected&quot; : &quot;&quot;\n    }&gt;${size.charAt(0).toUpperCase() + size.slice(1)}&lt;/option&gt;\n            `).\n\n    join(&quot;&quot;)}\n          &lt;/select&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control-group&quot;&gt;\n          &lt;label&gt;Border Radius&lt;/label&gt;\n          &lt;div class=&quot;slider-container&quot;&gt;\n            &lt;input \n              id=&quot;radius-slider&quot; \n              type=&quot;range&quot; \n              min=&quot;0&quot; \n              max=&quot;${this.borderRadiusOptions.length - 1}&quot; \n              value=&quot;${this.borderRadiusOptions.indexOf(this.borderRadius)}&quot; \n            /&gt;\n            &lt;div id=&quot;radius-value&quot; class=&quot;value-display&quot;&gt;${\n    this.borderRadius\n    }&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    this.addEventListeners();\n  }\n\n  // Helper to determine if a color is dark\n  isDarkColor(color) {\n    // Convert hex to RGB\n    const hex = color.replace(&quot;#&quot;, &quot;&quot;);\n    const r = parseInt(hex.substr(0, 2), 16);\n    const g = parseInt(hex.substr(2, 2), 16);\n    const b = parseInt(hex.substr(4, 2), 16);\n\n    // Calculate brightness\n    const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n    return brightness &lt; 128;\n  }}\n\n\n// Register the theme controller component\ncustomElements.define(&quot;theme-controller&quot;, ThemeController);\n\n// Register the components\ncustomElements.define(&quot;theme-provider&quot;, ThemeProvider);\ncustomElements.define(&quot;theme-consumer&quot;, ThemeConsumer);\ncustomElements.define(&quot;theme-toggler&quot;, ThemeToggler);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"declarative-shadow-dom-server-side-renaissance\">Declarative Shadow DOM: Server-Side Renaissance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#declarative-shadow-dom-server-side-renaissance\" aria-label=\"Link to this section\">#</a></h3>\n<p>One of the most significant limitations of Web Components has long been server-side rendering capabilities. The Declarative Shadow DOM (DSD) is changing this landscape, enabling Web Components to be rendered on the server before reaching the browser.</p>\n<p>Enhance.dev is built around this approach. Here is the syntax. Note the attribute is <code>shadowrootmode</code>; the older <code>shadowroot</code> spelling from early drafts no longer creates a shadow root (checked in Chrome 156):</p>\n<pre><code class=\"language-html\">&lt;my-card&gt;\n  &lt;template shadowrootmode=\"open\"&gt;\n    &lt;style&gt;:host { display: block; }&lt;/style&gt;\n    &lt;slot name=\"title\"&gt;&lt;/slot&gt;\n  &lt;/template&gt;\n  &lt;h1 slot=\"title\"&gt;Hello from the server!&lt;/h1&gt;\n&lt;/my-card&gt;\n</code></pre>\n<p>The browser builds the shadow tree while parsing the HTML, so the component paints before any JavaScript runs.</p>\n<p>Check out this interactive demo to see this pattern in action:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Declarative Shadow Dom</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;my-card counter=&quot;1&quot;&gt;\n      &lt;template shadowrootmode=&quot;open&quot;&gt;\n        &lt;style&gt;\n          :host {\n            display: block;\n            border: 1px solid var(--border-color, #ccc);\n            border-radius: 8px;\n            padding: 16px;\n            font-family: system-ui, sans-serif;\n            background-color: var(--background-color, #fff);\n            color: var(--text-color, #333);\n          }\n\n          .card-header {\n            border-bottom: 1px solid var(--divider-color, #eee);\n            padding-bottom: 10px;\n            margin-bottom: 10px;\n          }\n\n          ::slotted(h1) {\n            margin-top: 0;\n            color: var(--text-color, #333);\n          }\n          \n          .counter {\n            margin-top: 15px;\n            padding-top: 15px;\n            border-top: 1px solid var(--divider-color, #eee);\n          }\n          \n          button {\n            background-color: var(--background-color);\n            color: var(--text-color);\n            border: 1px solid var(--border-color);\n            padding: 5px 10px;\n            margin-right: 10px;\n            border-radius: 4px;\n          }\n        &lt;/style&gt;\n\n        &lt;div class=&quot;card-header&quot;&gt;\n          &lt;slot name=&quot;title&quot;&gt;Default Title&lt;/slot&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;card-body&quot;&gt;\n          &lt;slot&gt;Default content&lt;/slot&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;counter&quot;&gt;\n          &lt;p&gt;Count: &lt;span id=&quot;count&quot;&gt;0&lt;/span&gt;&lt;/p&gt;\n          &lt;button id=&quot;decrement&quot;&gt;-&lt;/button&gt;\n          &lt;button id=&quot;increment&quot;&gt;+&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/template&gt;\n\n      &lt;h1 slot=&quot;title&quot;&gt;Hello from Declarative Shadow DOM&lt;/h1&gt;\n      &lt;p&gt;\n        This card demonstrates Declarative Shadow DOM for server-side rendering.\n      &lt;/p&gt;\n    &lt;/my-card&gt;\n    \n    &lt;div class=&quot;explanation&quot;&gt;\n      &lt;h2&gt;Benefits of Declarative Shadow DOM&lt;/h2&gt;\n      &lt;p&gt;\n        Declarative Shadow DOM provides significant advantages over imperative shadow DOM creation:\n      &lt;/p&gt;\n      &lt;ul&gt;\n        &lt;li&gt;&lt;strong&gt;Server-Side Rendering:&lt;/strong&gt; Components with encapsulated styling can be rendered on the server, improving initial page load performance&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Zero Hydration Cost:&lt;/strong&gt; No JavaScript is required to initialize the shadow DOM, making components immediately interactive&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Progressive Enhancement:&lt;/strong&gt; Components work even before JavaScript loads or if JavaScript fails&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;SEO Friendly:&lt;/strong&gt; Content is available in the initial HTML, making it more accessible to search engines&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Simplified Component Definition:&lt;/strong&gt; No need for separate template files or string literals - the component structure is defined directly in HTML&lt;/li&gt;\n      &lt;/ul&gt;\n      &lt;p&gt;\n        The component above demonstrates these benefits while also implementing dark mode support,\n        showing how declarative shadow DOM can be both highly performant and user-friendly.\n      &lt;/p&gt;\n    &lt;/div&gt;    \n    &lt;h2 class=&quot;code-header&quot;&gt;Declarative Shadow DOM Code:&lt;/h2&gt;\n    &lt;pre&gt;&lt;code&gt;&amp;lt;my-card counter=&quot;1&quot; &amp;gt;\n  &amp;lt;template shadowrootmode=&quot;open&quot;&amp;gt;\n    &amp;lt;style&amp;gt;\n      :host {\n        display: block;\n        border: 1px solid var(--border-color, #ccc);\n        border-radius: 8px;\n        padding: 16px;\n        font-family: system-ui, sans-serif;\n        background-color: var(--background-color, #fff);\n        color: var(--text-color, #333);\n      }\n\n      .card-header {\n        border-bottom: 1px solid var(--divider-color, #eee);\n        padding-bottom: 10px;\n        margin-bottom: 10px;\n      }\n\n      ::slotted(h1) {\n        margin-top: 0;\n        color: var(--text-color, #333);\n      }\n      \n      .counter {\n        margin-top: 15px;\n        padding-top: 15px;\n        border-top: 1px solid var(--divider-color, #eee);\n      }\n      \n      button {\n        background-color: var(--background-color);\n        color: var(--text-color);\n        border: 1px solid var(--border-color);\n        padding: 5px 10px;\n        margin-right: 10px;\n        border-radius: 4px;\n      }\n    &amp;lt;/style&amp;gt;\n\n    &amp;lt;div class=&quot;card-header&quot;&amp;gt;\n      &amp;lt;slot name=&quot;title&quot;&amp;gt;Default Title&amp;lt;/slot&amp;gt;\n    &amp;lt;/div&amp;gt;\n    &amp;lt;div class=&quot;card-body&quot;&amp;gt;\n      &amp;lt;slot&amp;gt;Default content&amp;lt;/slot&amp;gt;\n    &amp;lt;/div&amp;gt;\n    &amp;lt;div class=&quot;counter&quot;&amp;gt;\n      &amp;lt;p&amp;gt;Count: &amp;lt;span id=&quot;count&quot;&amp;gt;0&amp;lt;/span&amp;gt;&amp;lt;/p&amp;gt;\n      &amp;lt;button id=&quot;decrement&quot;&amp;gt;-&amp;lt;/button&amp;gt;\n      &amp;lt;button id=&quot;increment&quot;&amp;gt;+&amp;lt;/button&amp;gt;\n    &amp;lt;/div&amp;gt;\n  &amp;lt;/template&amp;gt;\n\n  &amp;lt;h1 slot=&quot;title&quot;&amp;gt;Hello from Declarative Shadow DOM&amp;lt;/h1&amp;gt;\n  &amp;lt;p&amp;gt;\n    This card demonstrates Declarative Shadow DOM for server-side rendering.\n  &amp;lt;/p&amp;gt;\n&amp;lt;/my-card&amp;gt;&lt;/code&gt;&lt;/pre&gt;\n\n    &lt;div class=&quot;explanation&quot;&gt;\n      &lt;h2&gt;Adding JavaScript Logic with Declarative Shadow DOM&lt;/h2&gt;\n      &lt;p&gt;\n        While declarative shadow DOM provides the HTML structure and styling, you can still add JavaScript logic through custom elements:\n      &lt;/p&gt;\n      &lt;ul&gt;\n        &lt;li&gt;&lt;strong&gt;Progressive Enhancement:&lt;/strong&gt; The component renders with its structure and styling before JavaScript loads&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Logic Attachment:&lt;/strong&gt; JavaScript enhances the pre-rendered component with behavior after loading&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Works either way:&lt;/strong&gt; The custom element class works with both declaratively and imperatively created shadow DOM&lt;/li&gt;\n        &lt;li&gt;&lt;strong&gt;Attribute Reactivity:&lt;/strong&gt; Using attributes and the built-in lifecycle callbacks enables state management&lt;/li&gt;\n      &lt;/ul&gt;\n      &lt;p&gt;\n        This approach gives you the best of both worlds: instant rendering with declarative shadow DOM,\n        plus fully interactive behavior once JavaScript executes.\n      &lt;/p&gt;\n    &lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n        color-scheme: light dark;\n        --background-color: #ffffff;\n        --text-color: #333333;\n        --border-color: #cccccc;\n        --divider-color: #eeeeee;\n      }\n\n      @media (prefers-color-scheme: dark) {\n        :root {\n          --background-color: #222222;\n          --text-color: #f0f0f0;\n          --border-color: #555555;\n          --divider-color: #444444;\n        }\n      }\n\n      body {\n        background-color: var(--background-color);\n        color: var(--text-color);\n        margin: 20px;\n      }\n\n      pre {\n        background-color: var(--background-color);\n        border: 1px solid var(--border-color);\n        border-radius: 4px;\n        padding: 15px;\n        overflow: auto;\n        margin-top: 30px;\n      }\n      \n      code {\n        font-family: monospace;\n        color: var(--text-color);\n        white-space: pre;\n      }\n      \n      .code-header {\n        font-weight: bold;\n        margin-bottom: 10px;\n        margin-top: 30px;\n        color: var(--text-color);\n      }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Check and polyfill if necessary\nif (!HTMLTemplateElement.prototype.hasOwnProperty(&quot;shadowRootMode&quot;)) {\n  document.\n  querySelectorAll(&quot;template[shadowrootmode]&quot;).\n  forEach(template =&gt; {\n    const mode = template.getAttribute(&quot;shadowrootmode&quot;);\n    const shadowRoot = template.parentNode.attachShadow({ mode });\n    shadowRoot.appendChild(template.content);\n    template.remove();\n  });\n}\n\n// Register the element with behavior\ncustomElements.define(&quot;my-card&quot;, class extends HTMLElement {\n  static get observedAttributes() {\n    return [&#39;counter&#39;];\n  }\n\n  constructor() {\n    super();\n\n    // Get access to shadow DOM elements (works regardless of how shadow DOM was created)\n    this._setupShadowDOM();\n  }\n\n  _setupShadowDOM() {\n    // If the shadow root already exists (from declarative shadow DOM)\n    // we can use it directly, otherwise it would have been created by the polyfill\n    const shadowRoot = this.shadowRoot;\n\n    // Get references to elements in the shadow DOM\n    this._countDisplay = shadowRoot.querySelector(&#39;#count&#39;);\n    this._incrementBtn = shadowRoot.querySelector(&#39;#increment&#39;);\n    this._decrementBtn = shadowRoot.querySelector(&#39;#decrement&#39;);\n\n    // Add event listeners\n    this._incrementBtn.addEventListener(&#39;click&#39;, () =&gt; this._incrementCount());\n    this._decrementBtn.addEventListener(&#39;click&#39;, () =&gt; this._decrementCount());\n\n    // Initialize with attribute value\n    this._updateCount(parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;));\n  }\n\n  connectedCallback() {\n    // Make sure the count is updated with the latest attribute value\n    // This handles the case where attributes might have changed before connection\n    this._updateCount(parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;));\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;counter&#39; &amp;&amp; this._countDisplay) {\n      this._updateCount(parseInt(newValue || &#39;0&#39;));\n    }\n  }\n\n  _incrementCount() {\n    const current = parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;);\n    this.setAttribute(&#39;counter&#39;, current + 1);\n  }\n\n  _decrementCount() {\n    const current = parseInt(this.getAttribute(&#39;counter&#39;) || &#39;0&#39;);\n    this.setAttribute(&#39;counter&#39;, Math.max(0, current - 1));\n  }\n\n  _updateCount(value) {\n    if (this._countDisplay) {\n      this._countDisplay.textContent = value;\n    }\n  }});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"state-management-evolves\">State Management Evolves<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#state-management-evolves\" aria-label=\"Link to this section\">#</a></h3>\n<p>State management in complex Web Components can quickly become unwieldy. The community has answered with a few small patterns, shown below.</p>\n<p>Signals libraries offer a React-inspired state model without the framework dependencies, while Lit State provides reactive state management specifically tailored to component systems. Most exciting is the Custom State Pseudo-Class, which now ships in browsers and lets you style components based on their internal state. A component adds a state with <code>this.attachInternals().states.add('loading')</code>, and CSS matches it with <code>:state()</code>:</p>\n<pre><code class=\"language-css\">my-element:state(loading) {\n  opacity: 0.5;\n}\n</code></pre>\n<p>These are patterns you can write today. None of them is a browser feature.</p>\n<p>Here is another interactive example that uses <code>ElementInternals.states</code> and the <code>:state()</code> pseudo-class, styling the component from the page and from inside its own shadow root.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Custom State Pseudo-Class Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;state-component&gt;&lt;/state-component&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n\tfont-family: system-ui, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n\ttransition: background-color 0.3s, color 0.3s;\n}\n\n@media (prefers-color-scheme: dark) {\n\tbody {\n\t\tbackground-color: #121212;\n\t\tcolor: #e0e0e0;\n\t}\n}\n\n/* Custom states are styled from outside with :state(), with no attributes involved */\nstate-component:state(loading) {\n\topacity: 0.5;\n\tpointer-events: none;\n}\n\nstate-component:state(error) {\n\toutline: 3px solid #f44336;\n\toutline-offset: 4px;\n\tborder-radius: 8px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class StateComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // ElementInternals.states is a CustomStateSet: whatever it holds,\n    // CSS can match with :state(name), from outside or via :host(:state(name)).\n    this._internals = this.attachInternals();\n\n    // Initial state\n    this.state = &quot;idle&quot;;\n    this._internals.states.add(this.state);\n\n    // One bound listener, so it can be removed again\n    this.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);\n\n    // Auto-reset timer reference\n    this.resetTimer = null;\n\n    // Default auto-reset duration (ms)\n    this.resetDuration = 3000;\n\n    // Check dark mode preference\n    this.darkMode = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches;\n\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&#39;reset-duration&#39;, &#39;dark-mode&#39;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;reset-duration&#39; &amp;&amp; newValue) {\n      this.resetDuration = parseInt(newValue, 10);\n    } else if (name === &#39;dark-mode&#39;) {\n      this.darkMode = newValue === &#39;true&#39;;\n      this.render();\n    }\n  }\n\n  connectedCallback() {\n    // Listen for system color scheme changes\n    this.colorSchemeMedia = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;);\n    this.colorSchemeMedia.addEventListener(&#39;change&#39;, this.handleColorSchemeChange);\n  }\n\n  disconnectedCallback() {\n    // Clean up timers when element is removed\n    this.clearResetTimer();\n\n    // Remove event listener\n    if (this.colorSchemeMedia) {\n      this.colorSchemeMedia.removeEventListener(&#39;change&#39;, this.handleColorSchemeChange);\n    }\n  }\n\n  handleColorSchemeChange(e) {\n    this.darkMode = e.matches;\n    this.render();\n  }\n\n  clearResetTimer() {\n    if (this.resetTimer) {\n      clearTimeout(this.resetTimer);\n      this.resetTimer = null;\n\n      // Also clear the progress animation\n      const progressBar = this.shadowRoot.querySelector(&#39;.progress-bar&#39;);\n      if (progressBar) {\n        progressBar.style.animation = &#39;none&#39;;\n      }\n    }\n  }\n\n  setState(newState) {\n    this.state = newState;\n    this._internals.states.clear();\n    this._internals.states.add(newState);\n\n    // Clear any existing timers\n    this.clearResetTimer();\n\n    // Set automatic reset for non-idle states\n    if (newState !== &quot;idle&quot;) {\n      this.resetTimer = setTimeout(() =&gt; {\n        this.setState(&quot;idle&quot;);\n      }, this.resetDuration);\n\n      // Update progress bar animation duration after render\n      setTimeout(() =&gt; {\n        const progressBar = this.shadowRoot.querySelector(&#39;.progress-bar&#39;);\n        if (progressBar) {\n          progressBar.style.animation = `progress ${this.resetDuration / 1000}s linear forwards`;\n        }\n      }, 0);\n    }\n\n    this.render();\n  }\n\n  toggleDarkMode() {\n    this.darkMode = !this.darkMode;\n    this.setAttribute(&#39;dark-mode&#39;, this.darkMode);\n    this.render();\n  }\n\n  render() {\n    let icon, message, ariaLive;\n\n    switch (this.state) {\n      case &quot;loading&quot;:\n        icon = &quot;⏳&quot;;\n        message = &quot;Loading...&quot;;\n        ariaLive = &quot;polite&quot;;\n        break;\n      case &quot;success&quot;:\n        icon = &quot;✅&quot;;\n        message = &quot;Operation successful!&quot;;\n        ariaLive = &quot;assertive&quot;;\n        break;\n      case &quot;error&quot;:\n        icon = &quot;❌&quot;;\n        message = &quot;An error occurred.&quot;;\n        ariaLive = &quot;assertive&quot;;\n        break;\n      default: // idle\n        icon = &quot;⚪&quot;;\n        message = &quot;Component is idle. Click a button to change state.&quot;;\n        ariaLive = &quot;off&quot;;}\n\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          padding: 20px;\n          border: 1px solid #ddd;\n          border-radius: 8px;\n          transition: all 0.3s;\n          background-color: ${this.darkMode ? &#39;#222&#39; : &#39;#fff&#39;};\n          color: ${this.darkMode ? &#39;#e0e0e0&#39; : &#39;#333&#39;};\n        }\n        \n        h2 {\n          margin-top: 0;\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n        }\n        \n        .container {\n          display: flex;\n          align-items: center;\n          margin-bottom: 20px;\n        }\n        \n        .icon {\n          font-size: 32px;\n          margin-right: 16px;\n        }\n\n        :host(:state(success)) .message {\n          color: #4caf50;\n        }\n\n        :host(:state(error)) .message {\n          color: #f44336;\n        }\n        \n        .message {\n          font-size: 18px;\n        }\n        \n        .buttons {\n          display: grid;\n          grid-template-columns: repeat(4, 1fr);\n          gap: 10px;\n          margin-bottom: 15px;\n        }\n        \n        button {\n          padding: 8px;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n          transition: all 0.2s;\n        }\n        \n        button:hover {\n          filter: brightness(0.9);\n        }\n        \n        button:active {\n          transform: translateY(1px);\n        }\n        \n        button:focus {\n          outline: 2px solid #2196F3;\n          outline-offset: 2px;\n        }\n        \n        button.idle {\n          background-color: ${this.darkMode ? &#39;#555&#39; : &#39;#e0e0e0&#39;};\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n        }\n        \n        button.loading {\n          background-color: #FFC107;\n          color: ${this.darkMode ? &#39;#000&#39; : &#39;#333&#39;};\n        }\n        \n        button.success {\n          background-color: #4CAF50;\n          color: white;\n        }\n        \n        button.error {\n          background-color: #f44336;\n          color: white;\n        }\n        \n        button.theme-toggle {\n          background-color: ${this.darkMode ? &#39;#777&#39; : &#39;#ddd&#39;};\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n          margin-top: 10px;\n        }\n        \n        .settings {\n          display: flex;\n          flex-wrap: wrap;\n          align-items: center;\n          gap: 12px;\n          margin-bottom: 15px;\n        }\n\n        .settings input {\n          width: 4em;\n        }\n\n        .reset-info {\n          font-size: 14px;\n          margin: 10px 0;\n          color: ${this.darkMode ? &#39;#aaa&#39; : &#39;#666&#39;};\n          display: ${this.state === &quot;idle&quot; ? &quot;none&quot; : &quot;block&quot;};\n        }\n        \n        .progress-container {\n          height: 4px;\n          width: 100%;\n          background-color: ${this.darkMode ? &#39;#444&#39; : &#39;#eee&#39;};\n          margin-bottom: 15px;\n          border-radius: 2px;\n          overflow: hidden;\n          display: ${this.state === &quot;idle&quot; ? &quot;none&quot; : &quot;block&quot;};\n        }\n        \n        .progress-bar {\n          height: 100%;\n          width: 100%;\n          background-color: #2196F3;\n          transform-origin: left;\n        }\n        \n        @keyframes progress {\n          0% { transform: scaleX(1); }\n          100% { transform: scaleX(0); }\n        }\n        \n        .settings {\n          margin-top: 20px;\n          padding-top: 15px;\n          border-top: 1px solid ${this.darkMode ? &#39;#444&#39; : &#39;#eee&#39;};\n        }\n        \n        .config {\n          display: flex;\n          align-items: center;\n          gap: 10px;\n        }\n        \n        .config label {\n          font-size: 14px;\n        }\n        \n        .config input {\n          width: 70px;\n          background-color: ${this.darkMode ? &#39;#333&#39; : &#39;#fff&#39;};\n          color: ${this.darkMode ? &#39;#fff&#39; : &#39;#333&#39;};\n          border: 1px solid ${this.darkMode ? &#39;#555&#39; : &#39;#ddd&#39;};\n          padding: 4px;\n          border-radius: 4px;\n        }\n        \n        small {\n          color: ${this.darkMode ? &#39;#aaa&#39; : &#39;#666&#39;};\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;h2&gt;Custom State Pseudo-Class Demo&lt;/h2&gt;\n        &lt;p&gt;This demonstrates how state changes can be exposed to CSS.&lt;/p&gt;\n        \n        &lt;div class=&quot;progress-container&quot; aria-hidden=&quot;true&quot;&gt;\n          &lt;div class=&quot;progress-bar&quot;&gt;&lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;reset-info&quot; aria-live=&quot;polite&quot;&gt;\n          Will reset to idle in ${this.resetDuration / 1000} seconds\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;container&quot;&gt;\n          &lt;div class=&quot;icon&quot; aria-hidden=&quot;true&quot;&gt;${icon}&lt;/div&gt;\n          &lt;div class=&quot;message&quot; aria-live=&quot;${ariaLive}&quot;&gt;${message}&lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;buttons&quot; role=&quot;group&quot; aria-label=&quot;State control buttons&quot;&gt;\n          &lt;button class=&quot;idle&quot; id=&quot;set-idle&quot;&gt;Idle&lt;/button&gt;\n          &lt;button class=&quot;loading&quot; id=&quot;set-loading&quot;&gt;Loading&lt;/button&gt;\n          &lt;button class=&quot;success&quot; id=&quot;set-success&quot;&gt;Success&lt;/button&gt;\n          &lt;button class=&quot;error&quot; id=&quot;set-error&quot;&gt;Error&lt;/button&gt;\n        &lt;/div&gt;\n\n        &lt;div class=&quot;settings&quot;&gt;\n          &lt;button id=&quot;toggle-theme&quot; type=&quot;button&quot;&gt;${this.darkMode ? &#39;Light&#39; : &#39;Dark&#39;} theme&lt;/button&gt;\n          &lt;label&gt;\n            Reset after\n            &lt;input id=&quot;reset-time&quot; type=&quot;number&quot; min=&quot;1&quot; max=&quot;30&quot; value=&quot;${this.resetDuration / 1000}&quot;&gt;\n            seconds\n          &lt;/label&gt;\n        &lt;/div&gt;\n\n        &lt;p&gt;\n          &lt;small&gt;\n            This uses ElementInternals.states: the page styles\n            state-component:state(loading), and the component styles\n            :host(:state(success)) from inside its shadow root.\n          &lt;/small&gt;\n        &lt;/p&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.\n    querySelector(&quot;#set-idle&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;idle&quot;);\n    });\n\n    this.shadowRoot.\n    querySelector(&quot;#set-loading&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;loading&quot;);\n    });\n\n    this.shadowRoot.\n    querySelector(&quot;#set-success&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;success&quot;);\n    });\n\n    this.shadowRoot.\n    querySelector(&quot;#set-error&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.setState(&quot;error&quot;);\n    });\n\n    // Add theme toggle listener\n    this.shadowRoot.\n    querySelector(&quot;#toggle-theme&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.toggleDarkMode();\n    });\n\n    // Add reset duration input listener\n    this.shadowRoot.\n    querySelector(&quot;#reset-time&quot;).\n    addEventListener(&quot;change&quot;, e =&gt; {\n      const seconds = parseInt(e.target.value, 10);\n      if (seconds &gt; 0) {\n        this.resetDuration = seconds * 1000;\n        // If there&#39;s an active timer, restart it with new duration\n        if (this.state !== &quot;idle&quot;) {\n          this.setState(this.state);\n        }\n      }\n    });\n  }}\n\n\ncustomElements.define(&quot;state-component&quot;, StateComponent);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"micro-frontends-the-enterprise-adoption\">Micro-Frontends: The Enterprise Adoption<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#micro-frontends-the-enterprise-adoption\" aria-label=\"Link to this section\">#</a></h3>\n<p>A micro-frontend splits one page into parts that separate teams build and ship. Web components give each part a boundary that does not depend on a framework. Adobe's Spectrum Web Components and Microsoft's FAST are published as web components, so any of these parts can use them whatever framework it is written in.</p>\n<h2 id=\"community-momentum-tools-libraries\">Community Momentum: Tools &amp; Libraries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#community-momentum-tools-libraries\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"lit-and-tooling\">Lit and Tooling<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#lit-and-tooling\" aria-label=\"Link to this section\">#</a></h3>\n<p>Lit is the most widely used helper library in this post. It is a small base class that adds reactive properties and templates on top of custom elements. The Lit team also publishes experimental tools under the <code>@lit-labs</code> scope, such as <code>@lit-labs/analyzer</code>, which reads component source to describe it. Check Lit's own release notes for what is stable; this post makes no claims about future Lit versions.</p>\n<h3 id=\"scoped-custom-element-registries-solving-collisions\">Scoped Custom Element Registries: Solving Collisions<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#scoped-custom-element-registries-solving-collisions\" aria-label=\"Link to this section\">#</a></h3>\n<p>The registry of custom element names is global: one tag name can only point to one class for the whole page. Scoped custom element registries let a shadow root have its own registry. The snippet below ran correctly in Chrome 156. As of 26 October 2026 it also ships in Chrome and Edge 146 and Safari 26, but not in a stable Firefox, so feature-detect it (<code>typeof CustomElementRegistry</code>) and have a plan for the browsers without it. It does not work for customized built-in elements.</p>\n<pre><code class=\"language-javascript\">const registry = new CustomElementRegistry();\nregistry.define('my-element', MyElement);\nhost.attachShadow({ mode: 'open', customElementRegistry: registry });\ndocument.createElement('my-element', { customElementRegistry: registry });\n</code></pre>\n<p>Before browsers shipped it, the <code>@webcomponents/scoped-custom-element-registry</code> polyfill let teams try the pattern. Most apps never hit a tag-name clash, so this is a feature for the day two teams define <code>&lt;x-button&gt;</code> differently.</p>\n<p>Below is an interactive example with two components that share one tag name. The demo checks for support first, so in a browser without scoped registries (Firefox, as of today) you will see a notice instead of the components.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Scoped Custom Element Registries</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;demo-container&quot;&gt;\n\t&lt;header&gt;\n\t\t&lt;h2&gt;Scoped Custom Element Registries&lt;/h2&gt;\n\t\t&lt;button class=&quot;theme-toggle&quot; id=&quot;themeToggle&quot;&gt;Toggle Theme&lt;/button&gt;\n\t&lt;/header&gt;\n\t&lt;p&gt;\n\t\tThis demo uses scoped custom element registries, built into Chrome, Edge and Safari.\n\t\tThe same tag name can have completely different implementations in different component scopes.\n\t&lt;/p&gt;\n\n\t&lt;component-a&gt;&lt;/component-a&gt;\n\t&lt;component-b&gt;&lt;/component-b&gt;\n\t&lt;component-c&gt;&lt;/component-c&gt;\n\n\t&lt;div class=&quot;explanation&quot;&gt;\n\t\t&lt;h3&gt;About Scoped Custom Element Registries&lt;/h3&gt;\n\t\t&lt;p&gt;\n\t\t\tScoped Custom Element Registries are part of the future of web components,\n\t\t\tallowing developers to:\n\t\t&lt;/p&gt;\n\t\t&lt;ul&gt;\n\t\t\t&lt;li&gt;Avoid name collisions between components&lt;/li&gt;\n\t\t\t&lt;li&gt;Use the same tag name for different implementations in different contexts&lt;/li&gt;\n\t\t\t&lt;li&gt;Create truly encapsulated component systems&lt;/li&gt;\n\t\t\t&lt;li&gt;Support easier component versioning&lt;/li&gt;\n\t\t\t&lt;li&gt;Enable better component composition patterns&lt;/li&gt;\n\t\t&lt;/ul&gt;\n\t\t&lt;p&gt;\n\t\t\tEach component creates a &lt;code&gt;new CustomElementRegistry()&lt;/code&gt;, defines its own\n\t\t\t&lt;code&gt;custom-button&lt;/code&gt; in it, and passes it to\n\t\t\t&lt;code&gt;attachShadow({ customElementRegistry })&lt;/code&gt;.\n\t\t&lt;/p&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n\t--primary-color: #6200ee;\n\t--secondary-color: #03dac6;\n\t--background-color: #ffffff;\n\t--text-color: #333333;\n}\n\n.dark-mode {\n\t--primary-color: #bb86fc;\n\t--secondary-color: #03dac6;\n\t--background-color: #121212;\n\t--text-color: #e0e0e0;\n}\n\nbody {\n\tfont-family: system-ui, sans-serif;\n\tbackground-color: var(--background-color);\n\tcolor: var(--text-color);\n\ttransition: background-color 0.3s ease;\n}\n\n.demo-container {\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n}\n\ncomponent-a,\ncomponent-b,\ncomponent-c {\n\tdisplay: block;\n\tmargin: 20px 0;\n\tpadding: 20px;\n\tborder: 1px solid #ddd;\n\tborder-radius: 8px;\n}\n\nheader {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tmargin-bottom: 20px;\n}\n\n.theme-toggle {\n\tbackground-color: var(--primary-color);\n\tcolor: white;\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Scoped Custom Element Registries: each component owns a CustomElementRegistry,\n// passes it to attachShadow(), and creates elements from it. Nothing is added to\n// the global customElements registry, so the same tag can mean different things.\nconst scopedRegistriesSupported = (() =&gt; {\n  try {\n    new CustomElementRegistry();\n    return true;\n  } catch {\n    return false;\n  }\n})();\n\nclass ScopedElementsMixin {\n  constructor() {\n    this.registry = new CustomElementRegistry();\n  }\n\n  // Define a custom element in this component&#39;s scope\n  defineScopedElement(tagName, constructor) {\n    this.registry.define(tagName, constructor);\n    return constructor;\n  }\n\n  // Create an element from this scope&#39;s definitions\n  createScopedElement(tagName) {\n    return document.createElement(tagName, { customElementRegistry: this.registry });\n  }\n\n  // Check if a tag is defined in this scope\n  hasScopedElement(tagName) {\n    return this.registry.get(tagName) !== undefined;\n  }\n}\n\n\n// Component A with its own registry\nclass ComponentA extends HTMLElement {\n  constructor() {\n    super();\n    this.scopedElements = new ScopedElementsMixin();\n    this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.scopedElements.registry });\n\n    // Define version A of the custom button\n    class CustomButtonA extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = `\n          &lt;style&gt;\n            button {\n              background-color: var(--primary-color);\n              color: white;\n              border: none;\n              padding: 10px 20px;\n              border-radius: 4px;\n              font-size: 16px;\n              cursor: pointer;\n              transition: all 0.3s ease;\n            }\n            button:hover {\n              opacity: 0.9;\n              transform: translateY(-2px);\n            }\n          &lt;/style&gt;\n          &lt;button&gt;Component A Button&lt;/button&gt;\n        `;\n\n        const button = this.querySelector(&quot;button&quot;);\n        button.addEventListener(&quot;click&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Component A&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Register in the scoped registry\n    this.scopedElements.defineScopedElement(&quot;custom-button&quot;, CustomButtonA);\n\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .card {\n          padding: 16px;\n          background-color: var(--background-color);\n          border-radius: 8px;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        \n        h3 {\n          margin-top: 0;\n          color: var(--primary-color);\n        }\n        \n        .output {\n          margin-top: 10px;\n          font-style: italic;\n          color: var(--primary-color);\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;card&quot;&gt;\n        &lt;h3&gt;Component A&lt;/h3&gt;\n        &lt;p&gt;This component has its own scoped definition of custom-button:&lt;/p&gt;\n        &lt;div id=&quot;button-container&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;output&quot; id=&quot;output-a&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Create the scoped element\n    const button = this.scopedElements.createScopedElement(&quot;custom-button&quot;);\n    this.shadowRoot.querySelector(&quot;#button-container&quot;).appendChild(button);\n\n    // Listen for events from any component\n    document.addEventListener(&quot;component-action&quot;, e =&gt; {\n      const output = this.shadowRoot.querySelector(&quot;#output-a&quot;);\n      output.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n    });\n  }}\n\n\n// Component B with its own registry\nclass ComponentB extends HTMLElement {\n  constructor() {\n    super();\n    this.scopedElements = new ScopedElementsMixin();\n    this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.scopedElements.registry });\n\n    // Define version B of the custom button\n    class CustomButtonB extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = `\n          &lt;style&gt;\n            button {\n              background-color: var(--secondary-color);\n              color: black;\n              border: none;\n              padding: 12px 24px;\n              border-radius: 50px;\n              font-size: 16px;\n              font-weight: bold;\n              cursor: pointer;\n              transition: all 0.3s ease;\n            }\n            button:hover {\n              box-shadow: 0 4px 8px rgba(0,0,0,0.2);\n            }\n          &lt;/style&gt;\n          &lt;button&gt;Component B Button&lt;/button&gt;\n        `;\n\n        const button = this.querySelector(&quot;button&quot;);\n        button.addEventListener(&quot;click&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Component B&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Register in the scoped registry\n    this.scopedElements.defineScopedElement(&quot;custom-button&quot;, CustomButtonB);\n\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .card {\n          padding: 16px;\n          background-color: var(--background-color);\n          border-radius: 8px;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        \n        h3 {\n          margin-top: 0;\n          color: var(--secondary-color);\n        }\n        \n        .output {\n          margin-top: 10px;\n          font-style: italic;\n          color: var(--secondary-color);\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;card&quot;&gt;\n        &lt;h3&gt;Component B&lt;/h3&gt;\n        &lt;p&gt;This component has its own scoped definition of custom-button:&lt;/p&gt;\n        &lt;div id=&quot;button-container&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;output&quot; id=&quot;output-b&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Create the scoped element\n    const button = this.scopedElements.createScopedElement(&quot;custom-button&quot;);\n    this.shadowRoot.querySelector(&quot;#button-container&quot;).appendChild(button);\n\n    // Listen for events from any component\n    document.addEventListener(&quot;component-action&quot;, e =&gt; {\n      const output = this.shadowRoot.querySelector(&quot;#output-b&quot;);\n      output.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n    });\n  }}\n\n\n// Component C with nested scoped elements\nclass ComponentC extends HTMLElement {\n  constructor() {\n    super();\n    this.scopedElements = new ScopedElementsMixin();\n    this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.scopedElements.registry });\n\n    // Define version C of the custom button\n    class CustomButtonC extends HTMLElement {\n      connectedCallback() {\n        this.innerHTML = `\n                &lt;style&gt;\n                  button {\n                    background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n                    color: white;\n                    border: none;\n                    padding: 10px 20px;\n                    border-radius: 4px;\n                    font-size: 16px;\n                    cursor: pointer;\n                    box-shadow: 0 2px 5px rgba(0,0,0,0.2);\n                    transition: all 0.3s ease;\n                  }\n                  button:hover {\n                    transform: scale(1.05);\n                  }\n                &lt;/style&gt;\n                &lt;button&gt;Component C Button&lt;/button&gt;\n              `;\n\n        const button = this.querySelector(&quot;button&quot;);\n        button.addEventListener(&quot;click&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Component C&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Define a nested component with its own registry\n    class NestedComponent extends HTMLElement {\n      constructor() {\n        super();\n        this.nestedRegistry = new ScopedElementsMixin();\n        this.attachShadow({ mode: &quot;open&quot;, customElementRegistry: this.nestedRegistry.registry });\n\n        // Define a unique version of custom-button for nested component\n        class NestedButton extends HTMLElement {\n          connectedCallback() {\n            this.innerHTML = `\n                    &lt;style&gt;\n                      button {\n                        background-color: transparent;\n                        color: var(--text-color);\n                        border: 2px solid currentColor;\n                        padding: 8px 16px;\n                        border-radius: 4px;\n                        font-size: 14px;\n                        cursor: pointer;\n                      }\n                    &lt;/style&gt;\n                    &lt;button&gt;Nested Button&lt;/button&gt;\n                  `;\n\n            const button = this.querySelector(&quot;button&quot;);\n            button.addEventListener(&quot;click&quot;, () =&gt; {\n              const event = new CustomEvent(&quot;nested-action&quot;, {\n                bubbles: true,\n                composed: true });\n\n              this.dispatchEvent(event);\n            });\n          }}\n\n\n        // Register nested button in its own registry\n        this.nestedRegistry.defineScopedElement(&quot;custom-button&quot;, NestedButton);\n\n        // Render nested component\n        this.shadowRoot.innerHTML = `\n                &lt;style&gt;\n                  .nested {\n                    margin: 10px;\n                    padding: 10px;\n                    border: 1px dashed var(--text-color);\n                    border-radius: 4px;\n                  }\n                  h4 {\n                    margin-top: 0;\n                    font-size: 14px;\n                  }\n                &lt;/style&gt;\n                &lt;div class=&quot;nested&quot;&gt;\n                  &lt;h4&gt;Nested Component Scope&lt;/h4&gt;\n                  &lt;p&gt;This nested component has its own scoped registry:&lt;/p&gt;\n                  &lt;div id=&quot;nested-button-container&quot;&gt;&lt;/div&gt;\n                &lt;/div&gt;\n              `;\n\n        // Create and append the nested button\n        const nestedButton = this.nestedRegistry.createScopedElement(\n        &quot;custom-button&quot;);\n\n        this.shadowRoot.\n        querySelector(&quot;#nested-button-container&quot;).\n        appendChild(nestedButton);\n\n        // Listen for clicks on the nested button\n        this.shadowRoot.addEventListener(&quot;nested-action&quot;, () =&gt; {\n          const event = new CustomEvent(&quot;component-action&quot;, {\n            bubbles: true,\n            composed: true,\n            detail: { source: &quot;Nested Component&quot;, action: &quot;clicked&quot; } });\n\n          this.dispatchEvent(event);\n        });\n      }}\n\n\n    // Register components in C&#39;s scope\n    this.scopedElements.defineScopedElement(&quot;custom-button&quot;, CustomButtonC);\n    this.scopedElements.defineScopedElement(&quot;nested-component&quot;, NestedComponent);\n\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n            &lt;style&gt;\n              .card {\n                padding: 16px;\n                background-color: var(--background-color);\n                border-radius: 8px;\n                box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n              }\n              \n              h3 {\n                margin-top: 0;\n                background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n                -webkit-background-clip: text;\n                -webkit-text-fill-color: transparent;\n              }\n              \n              .component-registry {\n                margin-top: 15px;\n                font-size: 14px;\n              }\n              \n              .output {\n                margin-top: 10px;\n                font-style: italic;\n                background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n                -webkit-background-clip: text;\n                -webkit-text-fill-color: transparent;\n                font-weight: bold;\n              }\n            &lt;/style&gt;\n            \n            &lt;div class=&quot;card&quot;&gt;\n              &lt;h3&gt;Component C (Nested Components)&lt;/h3&gt;\n              &lt;p&gt;This component demonstrates nested scoped registries:&lt;/p&gt;\n              &lt;div id=&quot;button-container&quot;&gt;&lt;/div&gt;\n              &lt;div id=&quot;nested-container&quot;&gt;&lt;/div&gt;\n              &lt;div class=&quot;component-registry&quot;&gt;\n                &lt;p&gt;Registered elements in this scope: \n                  &lt;code&gt;custom-button&lt;/code&gt;, &lt;code&gt;nested-component&lt;/code&gt;\n                &lt;/p&gt;\n              &lt;/div&gt;\n              &lt;div class=&quot;output&quot; id=&quot;output-c&quot;&gt;&lt;/div&gt;\n            &lt;/div&gt;\n          `;\n\n    // Create the scoped elements\n    const button = this.scopedElements.createScopedElement(&quot;custom-button&quot;);\n    const nested = this.scopedElements.createScopedElement(&quot;nested-component&quot;);\n\n    this.shadowRoot.querySelector(&quot;#button-container&quot;).appendChild(button);\n    this.shadowRoot.querySelector(&quot;#nested-container&quot;).appendChild(nested);\n\n    // Listen for events from any component\n    document.addEventListener(&quot;component-action&quot;, e =&gt; {\n      const output = this.shadowRoot.querySelector(&quot;#output-c&quot;);\n      output.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n    });\n  }}\n\n\n// Register the components\nif (scopedRegistriesSupported) {\n  customElements.define(&quot;component-a&quot;, ComponentA);\n  customElements.define(&quot;component-b&quot;, ComponentB);\n  customElements.define(&quot;component-c&quot;, ComponentC);\n} else {\n  const notice = document.createElement(&quot;p&quot;);\n  notice.className = &quot;explanation&quot;;\n  notice.textContent = &quot;This browser does not support scoped custom element registries yet, so the components cannot be shown.&quot;;\n  document.querySelector(&quot;component-a&quot;).before(notice);\n}\n\n// Theme toggle functionality\nconst themeToggleButton = document.getElementById(&quot;themeToggle&quot;);\n\n// Function to update button text based on current theme\nfunction updateThemeButtonText() {\n  themeToggleButton.textContent = document.body.classList.contains(&quot;dark-mode&quot;) ?\n  &quot;Light Theme&quot; :\n  &quot;Dark Theme&quot;;\n}\n\n// Check user&#39;s system preference for dark mode\nfunction initializeThemeBasedOnPreference() {\n  // Check if user prefers dark mode\n  const prefersDarkMode = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).\n  matches;\n\n  // Set initial theme based on preference\n  if (prefersDarkMode) {\n    document.body.classList.add(&quot;dark-mode&quot;);\n  }\n\n  // Update button text to reflect initial state\n  updateThemeButtonText();\n}\n\n// Initialize theme when page loads\ninitializeThemeBasedOnPreference();\n\n// Theme toggle button click handler\nthemeToggleButton.addEventListener(&quot;click&quot;, () =&gt; {\n  document.body.classList.toggle(&quot;dark-mode&quot;);\n  updateThemeButtonText();\n});\n\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"testing-gets-easier\">Testing Gets Easier<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#testing-gets-easier\" aria-label=\"Link to this section\">#</a></h3>\n<p>Web Test Runner can run component tests in real browsers, and Storybook has a web-components renderer that can read a component's description from a Custom Elements Manifest. Both reduce the need for framework-specific test setups.</p>\n<h2 id=\"on-the-horizon-features-to-watch\">On the Horizon: Features to Watch<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#on-the-horizon-features-to-watch\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"manual-slot-assignment\">Manual Slot Assignment<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#manual-slot-assignment\" aria-label=\"Link to this section\">#</a></h3>\n<p>Normally the browser fills slots by matching <code>slot=\"name\"</code> attributes. Manual assignment lets script decide instead. It ran in Chrome 156, and it is Baseline widely available (all three engines since March 2023).</p>\n<pre><code class=\"language-javascript\">const root = host.attachShadow({ mode: 'open', slotAssignment: 'manual' });\nconst slot = document.createElement('slot');\nroot.append(slot);\n\nslot.assign(host.querySelector('h1')); // this child now fills the slot\n</code></pre>\n<p>You may see an <code>element.attachSlot('header')</code> call in older articles. That method does not exist (it is <code>undefined</code> in Chrome 156). Use <code>slotAssignment: 'manual'</code> and <code>slot.assign()</code>.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Which of these is NOT a real browser API?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>slot.assign(node)</code> with <code>slotAssignment: 'manual'</code></li>\n<li><code>new CustomElementRegistry()</code></li>\n<li><code>element.attachSlot('header')</code></li>\n<li><code>internals.states.add('loading')</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. <code>element.attachSlot('header')</code></p>\n<p><code>attachSlot</code> does not exist. Manual slotting is <code>slotAssignment: 'manual'</code> plus <code>slot.assign()</code>; scoped registries and custom states are real and ran in Chrome 156.</p>\n</div>\n</div>\n</section>\n<h3 id=\"scope-scoped-rules-not-shadow-dom\"><code>@scope</code>: Scoped Rules, Not Shadow DOM<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#scope-scoped-rules-not-shadow-dom\" aria-label=\"Link to this section\">#</a></h3>\n<p><code>@scope</code> limits where a CSS rule applies in a normal stylesheet. It became Baseline newly available in March 2026:</p>\n<pre><code class=\"language-css\">@scope (.card) {\n  h1 { color: red; } /* Only matches h1 elements inside .card */\n}\n</code></pre>\n<p>It works in the light DOM and does not create or cross a shadow boundary (in the test, an <code>h1</code> inside <code>.card</code> turned red and an <code>h1</code> outside stayed black). Styling across a shadow boundary is done with CSS custom properties, <code>::part()</code> and <code>::slotted()</code>, all of which ship today.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>What does <code>@scope (.card) { h1 { color: red; } }</code> do?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Creates a shadow root on every <code>.card</code></li>\n<li>Applies the rule only to <code>h1</code> elements inside <code>.card</code></li>\n<li>Lets outside CSS style the inside of any shadow root</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. Applies the rule only to <code>h1</code> elements inside <code>.card</code></p>\n<p><code>@scope</code> is a cascade feature for the normal DOM. Shadow roots are created with <code>attachShadow()</code>.</p>\n</div>\n</div>\n</section>\n<h3 id=\"webassembly-integration\">WebAssembly Integration<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#webassembly-integration\" aria-label=\"Link to this section\">#</a></h3>\n<p>WebAssembly is compiled code that runs in the browser. A custom element can call into it, for example to do heavy number crunching while keeping a plain HTML interface. Tools such as <code>wasm-bindgen</code> let Rust code talk to JavaScript. This is a combination of existing features, not a new web component standard.</p>\n<p>Check out a simulation of how we can bring WebAssembly into a web component below:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">WebAssembly Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;button id=&quot;dark-mode-toggle&quot; class=&quot;dark-mode-toggle&quot;&gt;🌙&lt;/button&gt;\n&lt;h1&gt;WebAssembly Component Integration&lt;/h1&gt;\n&lt;p&gt;\n\tThis component demonstrates how WebAssembly can be integrated with Web\n\tComponents for high-performance computation.\n&lt;/p&gt;\n\n&lt;wasm-component initial-value=&quot;5&quot; wasm-url=&quot;math_ops.wasm&quot;&gt;&lt;/wasm-component&gt;\n\n&lt;div style=&quot;margin-top: 30px&quot;&gt;\n\t&lt;h3&gt;Component API Usage&lt;/h3&gt;\n\t&lt;button id=&quot;external-set&quot;&gt;Set Value to 42&lt;/button&gt;\n\t&lt;button id=&quot;external-get&quot;&gt;Get Current Value&lt;/button&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n\tfont-family: system-ui, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n\ttransition: background-color 0.3s, color 0.3s;\n}\n\nbody.dark-mode {\n\tbackground-color: #121212;\n\tcolor: #e0e0e0;\n}\n\n.dark-mode-toggle {\n\tposition: absolute;\n\ttop: 20px;\n\tright: 20px;\n\tbackground: none;\n\tborder: none;\n\tcursor: pointer;\n\tfont-size: 24px;\n}\n\nh1,\nh3 {\n\ttransition: color 0.3s;\n}\n\nbody.dark-mode h1,\nbody.dark-mode h3 {\n\tcolor: #e0e0e0;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Dark mode functionality\nfunction initDarkMode() {\n\t// Check for user preference\n\tconst prefersDarkMode =\n\t\twindow.matchMedia &amp;&amp;\n\t\twindow.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;\n\n\t// Set initial mode based on preference\n\tif (prefersDarkMode) {\n\t\tdocument.body.classList.add(&quot;dark-mode&quot;);\n\t}\n\n\t// Add toggle listener\n\tdocument.getElementById(&quot;dark-mode-toggle&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\tdocument.body.classList.toggle(&quot;dark-mode&quot;);\n\t\tupdateDarkModeIcon();\n\t});\n\n\tupdateDarkModeIcon();\n}\n\nfunction updateDarkModeIcon() {\n\tconst isDarkMode = document.body.classList.contains(&quot;dark-mode&quot;);\n\tdocument.getElementById(&quot;dark-mode-toggle&quot;).textContent = isDarkMode\n\t\t? &quot;☀️&quot;\n\t\t: &quot;🌙&quot;;\n}\n\nclass WasmComponent extends HTMLElement {\n\tstatic observedAttributes = [&quot;initial-value&quot;, &quot;wasm-url&quot;];\n\n\tconstructor() {\n\t\tsuper();\n\t\tthis.attachShadow({ mode: &quot;open&quot; });\n\t\tthis.counter = 0;\n\t\tthis.isLoading = true;\n\t\tthis.hasError = false;\n\t\tthis.errorMessage = &quot;&quot;;\n\t\tthis.wasmModule = null;\n\t\tthis.MAX_VALUE = 1.9767639272283756e154; // Maximum allowed value\n\n\t\t// Check if dark mode is active when component is created\n\t\tthis.isDarkMode = document.body.classList.contains(&quot;dark-mode&quot;);\n\n\t\tthis.render();\n\t}\n\n\tconnectedCallback() {\n\t\t// Listen for dark mode changes on the body\n\t\tthis._darkModeObserver = new MutationObserver((mutations) =&gt; {\n\t\t\tmutations.forEach((mutation) =&gt; {\n\t\t\t\tif (mutation.attributeName === &quot;class&quot;) {\n\t\t\t\t\tthis.isDarkMode = document.body.classList.contains(&quot;dark-mode&quot;);\n\t\t\t\t\tthis.render();\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tthis._darkModeObserver.observe(document.body, { attributes: true });\n\n\t\t// Get initial values from attributes\n\t\tif (this.hasAttribute(&quot;initial-value&quot;)) {\n\t\t\tthis.counter = parseInt(this.getAttribute(&quot;initial-value&quot;), 10) || 0;\n\t\t\tthis.updateDisplay();\n\t\t}\n\n\t\t// Load the WASM module\n\t\tconst wasmUrl = this.getAttribute(&quot;wasm-url&quot;) || &quot;math_ops.wasm&quot;;\n\t\tthis.loadWasmModule(wasmUrl);\n\t}\n\n\tdisconnectedCallback() {\n\t\t// Clean up observer when component is removed\n\t\tif (this._darkModeObserver) {\n\t\t\tthis._darkModeObserver.disconnect();\n\t\t}\n\t}\n\n\tattributeChangedCallback(name, oldValue, newValue) {\n\t\tif (name === &quot;initial-value&quot; &amp;&amp; this.shadowRoot) {\n\t\t\tthis.counter = parseInt(newValue, 10) || 0;\n\t\t\tthis.updateDisplay();\n\t\t}\n\n\t\tif (name === &quot;wasm-url&quot; &amp;&amp; oldValue !== newValue &amp;&amp; this.isConnected) {\n\t\t\tthis.isLoading = true;\n\t\t\tthis.hasError = false;\n\t\t\tthis.render();\n\t\t\tthis.loadWasmModule(newValue);\n\t\t}\n\t}\n\n\tasync loadWasmModule(url) {\n\t\ttry {\n\t\t\tthis.isLoading = true;\n\t\t\tthis.render();\n\n\t\t\tawait new Promise((resolve) =&gt; setTimeout(resolve, 1000));\n\n\t\t\tthis.wasmModule = {\n\t\t\t\texports: {\n\t\t\t\t\tincrement: (value) =&gt; {\n\t\t\t\t\t\tconst result = value + 1;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\tdecrement: (value) =&gt; value - 1,\n\t\t\t\t\tsquare: (value) =&gt; {\n\t\t\t\t\t\tconst result = value * value;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\tcube: (value) =&gt; {\n\t\t\t\t\t\tconst result = value * value * value;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\tdouble: (value) =&gt; {\n\t\t\t\t\t\tconst result = value * 2;\n\t\t\t\t\t\treturn result &gt; this.MAX_VALUE ? this.MAX_VALUE : result;\n\t\t\t\t\t},\n\t\t\t\t\thalf: (value) =&gt; value / 2,\n\t\t\t\t\tfactorial: (value) =&gt; {\n\t\t\t\t\t\tif (value &lt;= 1) return 1;\n\t\t\t\t\t\tlet result = 1;\n\t\t\t\t\t\tfor (let i = 2; i &lt;= value; i++) {\n\t\t\t\t\t\t\tresult *= i;\n\t\t\t\t\t\t\tif (result &gt; this.MAX_VALUE) return this.MAX_VALUE;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn result;\n\t\t\t\t\t},\n\t\t\t\t\tisPrime: (value) =&gt; {\n\t\t\t\t\t\tif (value &lt;= 1) return 0;\n\t\t\t\t\t\tif (value &lt;= 3) return 1;\n\t\t\t\t\t\tif (value % 2 === 0 || value % 3 === 0) return 0;\n\t\t\t\t\t\tlet i = 5;\n\t\t\t\t\t\twhile (i * i &lt;= value) {\n\t\t\t\t\t\t\tif (value % i === 0 || value % (i + 2) === 0) return 0;\n\t\t\t\t\t\t\ti += 6;\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tthis.isLoading = false;\n\t\t\tthis.hasError = false;\n\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent(&quot;wasm-loaded&quot;, {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tcomposed: true,\n\t\t\t\t\tdetail: { success: true }\n\t\t\t\t})\n\t\t\t);\n\t\t} catch (error) {\n\t\t\tconsole.error(&quot;Failed to load WASM module:&quot;, error);\n\t\t\tthis.isLoading = false;\n\t\t\tthis.hasError = true;\n\t\t\tthis.errorMessage = error.message || &quot;Failed to load WebAssembly module&quot;;\n\n\t\t\tthis.dispatchEvent(\n\t\t\t\tnew CustomEvent(&quot;wasm-error&quot;, {\n\t\t\t\t\tbubbles: true,\n\t\t\t\t\tcomposed: true,\n\t\t\t\t\tdetail: { error }\n\t\t\t\t})\n\t\t\t);\n\t\t}\n\n\t\tthis.render();\n\t}\n\n\trender() {\n\t\tthis.shadowRoot.innerHTML = `\n              &lt;style&gt;\n                :host {\n                  display: block;\n                  padding: 20px;\n                  border: 1px solid ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  border-radius: 8px;\n                  background-color: ${this.isDarkMode ? &quot;#1e1e2f&quot; : &quot;#f8f5ff&quot;};\n                  color: ${this.isDarkMode ? &quot;#e0e0e0&quot; : &quot;inherit&quot;};\n                  transition: background-color 0.3s, border-color 0.3s, color 0.3s;\n                }\n                \n                h2 {\n                  margin-top: 0;\n                  color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  transition: color 0.3s;\n                }\n                \n                .display {\n                  font-size: 48px;\n                  text-align: center;\n                  margin: 20px 0;\n                  font-weight: bold;\n                  color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  transition: color 0.3s;\n                }\n                \n                .buttons {\n                  display: flex;\n                  flex-wrap: wrap;\n                  justify-content: center;\n                  gap: 10px;\n                  margin-bottom: 15px;\n                }\n                \n                button {\n                  background-color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                  color: ${this.isDarkMode ? &quot;#121212&quot; : &quot;white&quot;};\n                  border: none;\n                  padding: 10px 20px;\n                  border-radius: 4px;\n                  cursor: pointer;\n                  font-size: 16px;\n                  transition: background-color 0.2s, color 0.2s;\n                }\n                \n                button:hover {\n                  background-color: ${this.isDarkMode ? &quot;#b39ddb&quot; : &quot;#5e35b1&quot;};\n                }\n                \n                button:disabled {\n                  background-color: ${this.isDarkMode ? &quot;#4a4a4a&quot; : &quot;#b39ddb&quot;};\n                  cursor: not-allowed;\n                }\n                \n                .loading {\n                  text-align: center;\n                  padding: 20px;\n                  color: ${this.isDarkMode ? &quot;#9575cd&quot; : &quot;#673AB7&quot;};\n                }\n                \n                .loading::after {\n                  content: &quot;...&quot;;\n                  animation: dots 1.5s infinite;\n                }\n                \n                .error {\n                  background-color: ${this.isDarkMode ? &quot;#3c1f1f&quot; : &quot;#ffebee&quot;};\n                  border: 1px solid ${this.isDarkMode ? &quot;#b71c1c&quot; : &quot;#f44336&quot;};\n                  color: ${this.isDarkMode ? &quot;#f48fb1&quot; : &quot;#d32f2f&quot;};\n                  padding: 10px;\n                  border-radius: 4px;\n                  margin: 10px 0;\n                }\n                \n                .note {\n                  margin-top: 20px;\n                  font-size: 14px;\n                  color: ${this.isDarkMode ? &quot;#aaa&quot; : &quot;#666&quot;};\n                  font-style: italic;\n                }\n                \n                .prime-indicator {\n                  font-size: 14px;\n                  margin-top: 5px;\n                  text-align: center;\n                  font-weight: bold;\n                }\n                \n                .prime-yes {\n                  color: ${this.isDarkMode ? &quot;#81c784&quot; : &quot;#4caf50&quot;};\n                }\n                \n                .prime-no {\n                  color: ${this.isDarkMode ? &quot;#e57373&quot; : &quot;#f44336&quot;};\n                }\n                \n                @keyframes dots {\n                  0%, 20% { content: &quot;.&quot;; }\n                  40% { content: &quot;..&quot;; }\n                  60%, 100% { content: &quot;...&quot;; }\n                }\n              &lt;/style&gt;\n              \n              &lt;div&gt;\n                &lt;h2&gt;Advanced WebAssembly Component Demo&lt;/h2&gt;\n                &lt;p&gt;This demonstrates how a Web Component integrates with WebAssembly.&lt;/p&gt;\n                \n                ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `\n                  &lt;div class=&quot;loading&quot;&gt;Loading WebAssembly module&lt;/div&gt;\n                `\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: this.hasError\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `\n                  &lt;div class=&quot;error&quot;&gt;\n                    &lt;strong&gt;Error:&lt;/strong&gt; ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.errorMessage || &quot;Failed to load WebAssembly module&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                  &lt;/div&gt;\n                `\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: `\n                  &lt;div class=&quot;display&quot;&gt;${this.counter}&lt;/div&gt;\n                  \n                  &lt;div class=&quot;prime-indicator ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.wasmModule?.exports.isPrime(this.counter)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;prime-yes&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: &quot;prime-no&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;\n                    ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.wasmModule?.exports.isPrime(this.counter)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;This is a prime number!&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: this.counter &gt; 1\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;This is not a prime number&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: &quot;&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                  &lt;/div&gt;\n                  \n                  &lt;div class=&quot;buttons&quot;&gt;\n                    &lt;button id=&quot;decrement&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Decrement&lt;/button&gt;\n                    &lt;button id=&quot;increment&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Increment&lt;/button&gt;\n                    &lt;button id=&quot;double&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Double&lt;/button&gt;\n                    &lt;button id=&quot;half&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Half&lt;/button&gt;\n                    &lt;button id=&quot;square&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Square&lt;/button&gt;\n                    &lt;button id=&quot;cube&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Cube&lt;/button&gt;\n                    &lt;button id=&quot;factorial&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading || this.counter &gt; 12\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Factorial&lt;/button&gt;\n                    &lt;button id=&quot;reset&quot; ?disabled=&quot;${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}&quot;&gt;Reset&lt;/button&gt;\n                  &lt;/div&gt;\n                `\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                \n                &lt;p class=&quot;note&quot;&gt;\n                  ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tthis.isLoading\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;Loading WebAssembly module...&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: this.hasError\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? &quot;Failed to load WebAssembly module. Check console for details.&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: &quot;Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.&quot;\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n                &lt;/p&gt;\n              &lt;/div&gt;\n            `;\n\n\t\tif (!this.isLoading &amp;&amp; !this.hasError) {\n\t\t\tthis.shadowRoot.querySelector(&quot;#increment&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.increment(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#decrement&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.decrement(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#square&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.square(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#cube&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.cube(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#double&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.double(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t\tthis.checkMaxValueReached();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#half&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = this.wasmModule.exports.half(this.counter);\n\t\t\t\tthis.updateDisplay();\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#factorial&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tif (this.counter &lt;= 12) {\n\t\t\t\t\tthis.counter = this.wasmModule.exports.factorial(this.counter);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.checkMaxValueReached();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tthis.shadowRoot.querySelector(&quot;#reset&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\t\t\t\tthis.counter = 0;\n\t\t\t\tthis.updateDisplay();\n\t\t\t});\n\t\t}\n\t}\n\n\tcheckMaxValueReached() {\n\t\tif (this.counter &gt;= this.MAX_VALUE) {\n\t\t\tconsole.warn(&quot;Maximum value reached&quot;);\n\t\t\tconst note = this.shadowRoot.querySelector(&quot;.note&quot;);\n\t\t\tif (note) {\n\t\t\t\tnote.innerHTML =\n\t\t\t\t\t&#39;&lt;strong style=&quot;color: #ff9800;&quot;&gt;Warning: Maximum value reached. Some operations may be limited.&lt;/strong&gt;&#39;;\n\n\t\t\t\tsetTimeout(() =&gt; {\n\t\t\t\t\tnote.innerHTML =\n\t\t\t\t\t\t&quot;Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.&quot;;\n\t\t\t\t}, 3000);\n\t\t\t}\n\t\t}\n\t}\n\n\tupdateDisplay() {\n\t\tif (this.shadowRoot.querySelector(&quot;.display&quot;)) {\n\t\t\tlet displayText = this.counter;\n\t\t\tif (this.counter &gt; 1e15) {\n\t\t\t\tdisplayText = this.counter.toExponential(2);\n\t\t\t}\n\t\t\tthis.shadowRoot.querySelector(&quot;.display&quot;).textContent = displayText;\n\n\t\t\tconst primeIndicator = this.shadowRoot.querySelector(&quot;.prime-indicator&quot;);\n\t\t\tif (primeIndicator) {\n\t\t\t\tconst isPrime = this.wasmModule.exports.isPrime(this.counter);\n\t\t\t\tprimeIndicator.className = `prime-indicator ${\n\t\t\t\t\tisPrime ? &quot;prime-yes&quot; : &quot;prime-no&quot;\n\t\t\t\t}`;\n\t\t\t\tprimeIndicator.textContent = isPrime\n\t\t\t\t\t? &quot;This is a prime number!&quot;\n\t\t\t\t\t: this.counter &gt; 1\n\t\t\t\t\t? &quot;This is not a prime number&quot;\n\t\t\t\t\t: &quot;&quot;;\n\t\t\t}\n\n\t\t\tconst factorialBtn = this.shadowRoot.querySelector(&quot;#factorial&quot;);\n\t\t\tif (factorialBtn) {\n\t\t\t\tfactorialBtn.disabled = this.counter &gt; 12;\n\t\t\t}\n\t\t}\n\n\t\tthis.dispatchEvent(\n\t\t\tnew CustomEvent(&quot;value-changed&quot;, {\n\t\t\t\tbubbles: true,\n\t\t\t\tcomposed: true,\n\t\t\t\tdetail: { value: this.counter }\n\t\t\t})\n\t\t);\n\t}\n\n\tgetValue() {\n\t\treturn this.counter;\n\t}\n\n\tsetValue(value) {\n\t\tconst parsedValue = parseInt(value, 10) || 0;\n\t\tthis.counter = parsedValue &gt; this.MAX_VALUE ? this.MAX_VALUE : parsedValue;\n\t\tthis.updateDisplay();\n\t\tthis.checkMaxValueReached();\n\t}\n}\n\ncustomElements.define(&quot;wasm-component&quot;, WasmComponent);\n\ndocument.addEventListener(&quot;DOMContentLoaded&quot;, initDarkMode);\n\ndocument.getElementById(&quot;external-set&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\tconst component = document.querySelector(&quot;wasm-component&quot;);\n\tcomponent.setValue(42);\n});\n\ndocument.getElementById(&quot;external-get&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n\tconst component = document.querySelector(&quot;wasm-component&quot;);\n\talert(`Current value: ${component.getValue()}`);\n});\n\ndocument\n\t.querySelector(&quot;wasm-component&quot;)\n\t.addEventListener(&quot;value-changed&quot;, (e) =&gt; {\n\t\tconsole.log(&quot;Value changed:&quot;, e.detail.value);\n\t});\n\ndocument\n\t.querySelector(&quot;wasm-component&quot;)\n\t.addEventListener(&quot;wasm-loaded&quot;, (e) =&gt; {\n\t\tconsole.log(&quot;WASM module loaded successfully&quot;);\n\t});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/the-future-of-web-components/\">the original article</a>.</p></figure>\n<h2 id=\"challenges-that-remain\">Challenges That Remain<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#challenges-that-remain\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><strong>Frameworks.</strong> React passes unknown props to custom elements as attributes in version 18 and earlier (checked with 18.3.1 in <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the post on frameworks</a>). React 19 changed this and is not covered here.</li>\n<li><strong>Accessibility.</strong> Custom elements do not get built-in semantics. <code>ElementInternals.role</code> and the ARIA properties let a component declare its role; <code>internals.role = 'button'</code> worked in Chrome 156. A property named <code>accessibleRole</code> does not exist.</li>\n<li><strong>Learning.</strong> Documentation and examples spread across libraries remain the main barrier for newcomers.</li>\n</ul>\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/the-future-of-web-components/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>You can now tell which Web Component features are safe to use and which are not. Use <code>::part()</code>, <code>:state()</code>, Declarative Shadow DOM (<code>shadowrootmode</code>), manual slotting, constructable stylesheets and custom events today. Check <code>@scope</code> against your oldest supported browser, and treat scoped registries as a feature for Chrome, Edge and Safari until Firefox ships it. When an article names a method you have not seen, search for it on MDN or run it in your console before building on it, as <code>attachSlot</code> shows.</p>\n<p>For a next step, build a small component that uses one of the \"ships today\" rows from the table above. To follow standards work, the <a href=\"https://www.w3.org/community/webcomponents/\">W3C Web Components Community Group</a> publishes its discussions.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/the-future-of-web-components/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "The Future of Web Components\nCanonical: https://mnixblog.netlify.app/the-future-of-web-components/\nWhere Innovation Meets Standardization\nWhich Web Component features can you use today, and which are still ideas? This post sorts them, with a status table you can check against your own browser.\nSome features are ready now, some work in only some browsers, and a few are only proposals. Knowing which is which keeps us from building on something that does not exist.\nPrerequisites in two lines: a custom element is a JavaScript class registered with customElements.define(), and a shadow root gives it private markup and styles. Everything below extends those two ideas.\nWhat ships today and what does not\n\"Works\" in the second column means a small script ran in headless Chrome 156 while editing this post. Firefox and Safari were not tested by us. The Status column was checked against webstatus.dev (https://webstatus.dev/) and MDN on 26 October 2026, so treat it as a snapshot and look again before you rely on a row.\nFeature\nIn Chrome 156\nStatus, Oct 2026\n::part(): style what a component exposes\nWorks\nShips today\n:state(): style a component by its own state\nWorks\nBaseline newly available\nDeclarative Shadow DOM: shadowrootmode\nWorks\nBaseline widely available\nManual slot assignment: slot.assign()\nWorks\nBaseline widely available\n@scope: limit where a CSS rule applies\nWorks\nBaseline newly available\nScoped registries\nWorks\nChrome, Edge, Safari only\nattachSlot()\nundefined\nNot an API\naccessibleRole property\nNot present\nNot an API\nDeclarative custom elements\nNot testable\nProposal\nThe dates behind the Status column, from webstatus.dev and MDN on 26 October 2026:\n:state(): newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4).\nDeclarative Shadow DOM: newly available in February 2024 (Chrome 111, Firefox 123, Safari 16.4) and widely available since August 2026. The older shadowroot attribute no longer works.\nManual slot assignment: widely available in all three engines since March 2023.\n@scope: newly available since March 2026.\nScoped registries: Chrome and Edge 146, Safari 26. Firefox has not shipped it. It is in Nightly, and Mozilla has proposed shipping it in Firefox 159, so check before you rely on it.\n::part() was not re-checked for this update.\nThe Rising Tide: Emerging Trends\nThe rest of this post covers libraries, state and a few patterns. Most of it is built on the standards above, not added to the browser, and none of it is needed to write a custom element.\nWeb Component Libraries\nYou do not need a library to write a custom element, and every example above works without one. A library saves boilerplate for the parts you end up writing again and again.\nLit, from Google, is a small base class that adds reactive properties and templates on top of custom elements. Stencil is a compiler: you write TypeScript and JSX and it produces standards-based custom elements. FAST and Shoelace are the other two names that come up most. Each project changes its name, its version and its advice over time, so check its own site for its current state before you pick one. We did not re-check them for this update.\nThe cost is the usual one. A library is one more thing to learn and to keep up to date, and shadow DOM still makes global styling harder whichever one you choose.\nIs State Management Going Native?: Beyond Properties and Attributes\nThe browser gives you less state management than a framework does, and that is fine. The part it does give you is :state(): a component can expose states such as \"loading\" or \"expanded\" to CSS without adding classes the page could overwrite. It has been Baseline newly available since May 2024 (Chrome 125, Firefox 126, Safari 17.4). Stores, signals and reducers are libraries and patterns, not platform features, and the examples below are patterns you can write yourself.\nHTML\n<user-profile></user-profile>\nCSS\nbody {\nmargin: 0;\npadding: 20px;\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\ntransition: background-color 0.3s ease, color 0.3s ease;\n}\n@media (prefers-color-scheme: dark) {\nbody {\nbackground-color: #121212;\ncolor: #f0f0f0;\n}\n}\n@media (prefers-color-scheme: light) {\nbody {\nbackground-color: #f8f9fa;\ncolor: #333;\n}\n}\nJavaScript\n// Simple state management pattern for Web Components\nclass UserProfile extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Initialize state\nthis.state = {\nname: 'Guest User',\nrole: 'Visitor',\nisLoggedIn: false,\ndarkMode: window.matchMedia('(prefers-color-scheme: dark)').matches };\n// Listen for changes in color scheme preference\nthis.colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');\nthis.colorSchemeMedia.addEventListener('change', e => {\nthis.setState({ darkMode: e.matches });\n});\nthis.render();\n}\n// State management method\nsetState(newState) {\nthis.state = { ...this.state, ...newState };\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: system-ui, sans-serif;\nmax-width: 300px;\npadding: 20px;\nborder: 1px solid ${this.state.darkMode ? '#555' : '#ddd'};\nborder-radius: 8px;\nbackground-color: ${this.state.darkMode ? '#222' : '#fff'};\ncolor: ${this.state.darkMode ? '#eee' : '#333'};\n}\n.profile {\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\n}\n.avatar {\nwidth: 80px;\nheight: 80px;\nborder-radius: 50%;\nbackground-color: ${this.state.isLoggedIn ? '#4CAF50' : this.state.darkMode ? '#555' : '#ccc'};\ndisplay: flex;\nalign-items: center;\njustify-content: center;\nmargin-bottom: 16px;\ncolor: white;\nfont-size: 32px;\n}\n.name {\nfont-size: 20px;\nfont-weight: bold;\nmargin-bottom: 4px;\n}\n.role {\ncolor: ${this.state.darkMode ? '#aaa' : '#666'};\nmargin-bottom: 16px;\n}\nbutton {\nbackground-color: ${this.state.isLoggedIn ? '#f44336' : '#4CAF50'};\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\n}\nbutton:hover {\nopacity: 0.9;\n}\n</style>\n<div class=\"profile\">\n<div class=\"avatar\">${this.state.name.charAt(0)}</div>\n<div class=\"name\">${this.state.name}</div>\n<div class=\"role\">${this.state.role}</div>\n<button id=\"toggle-login\">\n${this.state.isLoggedIn ? 'Logout' : 'Login'}\n</button>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('#toggle-login').addEventListener('click', () => {\nif (this.state.isLoggedIn) {\nthis.setState({\nname: 'Guest User',\nrole: 'Visitor',\nisLoggedIn: false });\n} else {\nthis.setState({\nname: 'Jane Doe',\nrole: 'Administrator',\nisLoggedIn: true });\n}\n});\n}\ndisconnectedCallback() {\n// Clean up event listener when component is removed\nif (this.colorSchemeMedia) {\nthis.colorSchemeMedia.removeEventListener('change', this.handleColorSchemeChange);\n}\n}}\ncustomElements.define('user-profile', UserProfile);\nState Management in Web Components on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nContext: Sharing State Without Tight Coupling\nA context-style provider is a pattern, not a platform feature. A parent component holds a value, and the components inside it ask for that value by dispatching an event, so the provider and its consumers do not need to know about each other. A ThemeProvider is the usual example, and the one below is small enough to read in a minute. For plain style values, CSS custom properties are still simpler.\nHTML\n<theme-provider>\n<theme-consumer></theme-consumer>\n<theme-toggler></theme-toggler>\n<theme-controller></theme-controller>\n</theme-provider>\nJavaScript\n// Context-like pattern for Web Components\nclass ThemeContext {\nconstructor() {\n// Check for user preference\nconst prefersDarkMode =\nwindow.matchMedia &&\nwindow.matchMedia(\"(prefers-color-scheme: dark)\").matches;\nthis.theme = prefersDarkMode ? \"dark\" : \"light\";\nthis.accentColor = \"#2196F3\";\nthis.fontSize = \"medium\";\nthis.borderRadius = \"4px\";\nthis.listeners = new Set();\n// Track the last changed setting to help with animations\nthis.lastChanged = null;\n// Listen for system theme changes\nthis.mediaQuery = window.matchMedia(\"(prefers-color-scheme: dark)\");\nthis.handleSystemThemeChange = e => {\nthis.setTheme(e.matches ? \"dark\" : \"light\");\n};\n// Add listener for system theme changes if supported\nif (this.mediaQuery.addEventListener) {\nthis.mediaQuery.addEventListener(\"change\", this.handleSystemThemeChange);\n}\n// Apply initial theme to body/html/root\nthis.applyThemeToDocument(this.theme);\n}\ngetTheme() {\nreturn this.theme;\n}\nsetTheme(newTheme) {\nthis.theme = newTheme;\nthis.lastChanged = \"theme\";\nthis.applyThemeToDocument(newTheme);\nthis.notifyListeners();\n}\napplyThemeToDocument(theme) {\n// Set theme on html and body elements\ndocument.documentElement.style.backgroundColor =\ntheme === \"light\" ? \"#f5f5f5\" : \"#121212\";\ndocument.documentElement.style.transition = \"background-color 0.3s\";\ndocument.body.style.backgroundColor =\ntheme === \"light\" ? \"#f5f5f5\" : \"#121212\";\ndocument.body.style.color = theme === \"light\" ? \"#333333\" : \"#ffffff\";\ndocument.body.style.transition = \"background-color 0.3s, color 0.3s\";\n// Add/remove class on document root for additional styling hooks\nif (theme === \"dark\") {\ndocument.documentElement.classList.add(\"dark-theme\");\n} else {\ndocument.documentElement.classList.remove(\"dark-theme\");\n}\n}\ngetAccentColor() {\nreturn this.accentColor;\n}\nsetAccentColor(newColor) {\nthis.accentColor = newColor;\nthis.lastChanged = \"accentColor\";\nthis.notifyListeners();\n}\ngetFontSize() {\nreturn this.fontSize;\n}\nsetFontSize(newSize) {\nthis.fontSize = newSize;\nthis.lastChanged = \"fontSize\";\nthis.notifyListeners();\n}\ngetBorderRadius() {\nreturn this.borderRadius;\n}\nsetBorderRadius(newRadius) {\nthis.borderRadius = newRadius;\nthis.lastChanged = \"borderRadius\";\nthis.notifyListeners();\n}\ngetLastChanged() {\nreturn this.lastChanged;\n}\n// Get the current theme as a complete object\ngetCurrentThemeSettings() {\nreturn {\ntheme: this.theme,\naccentColor: this.accentColor,\nfontSize: this.fontSize,\nborderRadius: this.borderRadius };\n}\nsubscribe(listener) {\nthis.listeners.add(listener);\n}\nunsubscribe(listener) {\nthis.listeners.delete(listener);\n}\nnotifyListeners() {\nthis.listeners.forEach((listener) =>\nlistener(this.getCurrentThemeSettings()));\n}\ndisconnectMediaQuery() {\n// Clean up media query listener if it exists\nif (this.mediaQuery && this.mediaQuery.removeEventListener) {\nthis.mediaQuery.removeEventListener(\"change\", this.handleSystemThemeChange);\n}\n}}\n// Global context instance\nconst themeContext = new ThemeContext();\n// Add window unload handler to clean up listeners\nwindow.addEventListener(\"unload\", () => {\nthemeContext.disconnectMediaQuery();\n});\n// Provider component\nclass ThemeProvider extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: system-ui, sans-serif;\npadding: 20px;\nbackground-color: ${\nthemeContext.getTheme() === \"light\" ? \"#ffffff\" : \"#333333\"\n};\ncolor: ${themeContext.getTheme() === \"light\" ? \"#333333\" : \"#ffffff\"};\nborder-radius: 8px;\ntransition: background-color 0.3s, color 0.3s;\n}\n.container {\nmax-width: 600px;\nmargin: 0 auto;\n}\n</style>\n<div class=\"container\">\n<h2>Theme Context Demo</h2>\n<slot></slot>\n</div>\n`;\n// Update when theme changes\nthis.updateTheme = settings => {\nconst theme = settings.theme;\nconst styleElement = this.shadowRoot.querySelector(\"style\");\n// Update the style element with new theme colors\nstyleElement.textContent = `\n:host {\ndisplay: block;\nfont-family: system-ui, sans-serif;\npadding: 20px;\nbackground-color: ${theme === \"light\" ? \"#ffffff\" : \"#333333\"};\ncolor: ${theme === \"light\" ? \"#333333\" : \"#ffffff\"};\nborder-radius: 8px;\ntransition: background-color 0.3s, color 0.3s;\n}\n.container {\nmax-width: 600px;\nmargin: 0 auto;\n}\n`;\n};\nthemeContext.subscribe(this.updateTheme);\n}\ndisconnectedCallback() {\nthemeContext.unsubscribe(this.updateTheme);\n}}\n// Consumer component\nclass ThemeConsumer extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.render();\n// Update when theme changes\nthis.updateTheme = settings => {\n// Store which value changed to highlight it for the user\nthis.lastChanged = themeContext.getLastChanged();\nthis.render();\n// Reset highlight after animation completes\nsetTimeout(() => {\nthis.lastChanged = null;\nthis.render();\n}, 1500);\n};\nthemeContext.subscribe(this.updateTheme);\n}\ndisconnectedCallback() {\nthemeContext.unsubscribe(this.updateTheme);\n}\nrender() {\nconst settings = themeContext.getCurrentThemeSettings();\nconst theme = settings.theme;\nconst accentColor = settings.accentColor;\nconst fontSize = settings.fontSize;\nconst borderRadius = settings.borderRadius;\nconst lastChanged = this.lastChanged;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 16px 0;\npadding: 16px;\nborder-radius: ${borderRadius};\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#f9f9f9\" : \"#444\"};\ncolor: ${theme === \"light\" ? \"#333\" : \"#eee\"};\ntransition: all 0.3s;\n}\nh3 {\nmargin-top: 0;\nmargin-bottom: 12px;\ncolor: ${accentColor};\nfont-size: ${\nfontSize === \"small\" ?\n\"16px\" :\nfontSize === \"medium\" ?\n\"18px\" :\n\"20px\"\n};\ntransition: all 0.3s;\n}\n.theme-properties {\ndisplay: grid;\ngrid-template-columns: 120px 1fr;\ngrid-gap: 8px;\n}\n.property-name {\nfont-weight: bold;\nopacity: 0.8;\n}\n.property-value {\nfont-family: monospace;\npadding: 2px 6px;\nborder-radius: ${borderRadius};\nbackground-color: ${\ntheme === \"light\" ? \"rgba(0,0,0,0.05)\" : \"rgba(255,255,255,0.1)\"\n};\ntransition: all 0.3s;\n}\n.property-highlight {\nposition: relative;\nanimation: pulse 1.5s ease-in-out;\n}\n.property-highlight::before {\ncontent: \"\";\nposition: absolute;\ntop: 0;\nright: 0;\nbottom: 0;\nleft: 0;\nbackground-color: ${accentColor};\nopacity: 0.2;\nborder-radius: ${borderRadius};\nz-index: -1;\n}\n.color-preview {\ndisplay: inline-block;\nwidth: 14px;\nheight: 14px;\nborder-radius: 50%;\nmargin-right: 6px;\nvertical-align: middle;\n}\n.demo-text {\nmargin-top: 16px;\npadding: 12px;\nborder-radius: ${borderRadius};\nborder-left: 4px solid ${accentColor};\nfont-size: ${\nfontSize === \"small\" ?\n\"14px\" :\nfontSize === \"medium\" ?\n\"16px\" :\n\"18px\"\n};\nbackground-color: ${\ntheme === \"light\" ? \"rgba(0,0,0,0.02)\" : \"rgba(255,255,255,0.05)\"\n};\ntransition: all 0.3s;\n}\n@keyframes pulse {\n0% { transform: scale(1); }\n50% { transform: scale(1.03); }\n100% { transform: scale(1); }\n}\n</style>\n<div>\n<h3>Theme State Observer</h3>\n<div class=\"theme-properties\">\n<span class=\"property-name\">Theme:</span>\n<span class=\"property-value ${\nlastChanged === \"theme\" ? \"property-highlight\" : \"\"\n}\">\n${theme}\n</span>\n<span class=\"property-name\">Accent Color:</span>\n<span class=\"property-value ${\nlastChanged === \"accentColor\" ? \"property-highlight\" : \"\"\n}\">\n<span class=\"color-preview\" style=\"background-color: ${accentColor};\"></span>\n${accentColor}\n</span>\n<span class=\"property-name\">Font Size:</span>\n<span class=\"property-value ${\nlastChanged === \"fontSize\" ? \"property-highlight\" : \"\"\n}\">\n${fontSize}\n</span>\n<span class=\"property-name\">Border Radius:</span>\n<span class=\"property-value ${\nlastChanged === \"borderRadius\" ? \"property-highlight\" : \"\"\n}\">\n${borderRadius}\n</span>\n</div>\n<div class=\"demo-text\">\nThis text demonstrates the current theme settings. The border radius, font size,\nand colors are all controlled by the theme context.\n</div>\n</div>\n`;\n}}\n// Theme toggler component\nclass ThemeToggler extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// Create bound event handler to avoid recreating it\nthis.handleToggle = this.handleToggle.bind(this);\n// Subscribe to theme changes\nthis.updateTheme = () => {\nthis.render();\n};\nthemeContext.subscribe(this.updateTheme);\nthis.render();\n}\ndisconnectedCallback() {\n// Clean up event listeners and subscriptions\nthemeContext.unsubscribe(this.updateTheme);\nthis.removeToggleListener();\n}\nremoveToggleListener() {\nconst button = this.shadowRoot.querySelector(\"#theme-toggle\");\nif (button) {\nbutton.removeEventListener(\"click\", this.handleToggle);\n}\n}\nhandleToggle() {\nconst currentTheme = themeContext.getTheme();\nthemeContext.setTheme(currentTheme === \"light\" ? \"dark\" : \"light\");\n}\nrender() {\n// Clean up previous event listeners before re-rendering\nthis.removeToggleListener();\nconst theme = themeContext.getTheme();\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 16px 0;\npadding: 16px;\nborder-radius: 4px;\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#f9f9f9\" : \"#444\"};\ntransition: background-color 0.3s, border-color 0.3s;\n}\nbutton {\nbackground-color: ${theme === \"light\" ? \"#4CAF50\" : \"#66BB6A\"};\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s;\n}\nbutton:hover {\nbackground-color: ${theme === \"light\" ? \"#3e8e41\" : \"#4CAF50\"};\nbox-shadow: 0 2px 4px rgba(0,0,0,0.2);\n}\n</style>\n<div>\n<p>Click the button to toggle the theme:</p>\n<button id=\"theme-toggle\">\nSwitch to ${theme === \"light\" ? \"Dark\" : \"Light\"} Theme\n</button>\n</div>\n`;\n// Add event listener\nthis.shadowRoot.\nquerySelector(\"#theme-toggle\").\naddEventListener(\"click\", this.handleToggle);\n}}\n// Theme controller component\nclass ThemeController extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// Get current theme settings\nthis.theme = themeContext.getTheme();\nthis.accentColor = themeContext.getAccentColor();\nthis.fontSize = themeContext.getFontSize();\nthis.borderRadius = themeContext.getBorderRadius();\n// Define preset accent colors\nthis.accentColors = [\n\"#2196F3\", // Blue (default)\n\"#F44336\", // Red\n\"#4CAF50\", // Green\n\"#FF9800\", // Orange\n\"#9C27B0\", // Purple\n\"#00BCD4\", // Cyan\n\"#607D8B\" // Blue Grey\n];\n// Define preset font sizes\nthis.fontSizes = [\"small\", \"medium\", \"large\"];\n// Define preset border radius values\nthis.borderRadiusOptions = [\"0px\", \"4px\", \"8px\", \"12px\", \"16px\"];\nthis.render();\n// Update when theme changes\nthis.updateTheme = settings => {\nthis.theme = settings.theme;\nthis.accentColor = settings.accentColor;\nthis.fontSize = settings.fontSize;\nthis.borderRadius = settings.borderRadius;\nthis.render();\n};\nthemeContext.subscribe(this.updateTheme);\n}\ndisconnectedCallback() {\nthemeContext.unsubscribe(this.updateTheme);\n}\naddEventListeners() {\n// Theme toggle\nconst themeToggle = this.shadowRoot.getElementById(\"theme-toggle\");\nthemeToggle.addEventListener(\"click\", () => {\nthemeContext.setTheme(this.theme === \"light\" ? \"dark\" : \"light\");\n});\n// Accent color selection\nconst colorButtons = this.shadowRoot.querySelectorAll(\".color-option\");\ncolorButtons.forEach(button => {\nbutton.addEventListener(\"click\", () => {\nconst color = button.getAttribute(\"data-color\");\nthemeContext.setAccentColor(color);\n});\n});\n// Font size selection\nconst fontSizeSelect = this.shadowRoot.getElementById(\"font-size-select\");\nfontSizeSelect.value = this.fontSize;\nfontSizeSelect.addEventListener(\"change\", () => {\nthemeContext.setFontSize(fontSizeSelect.value);\n});\n// Border radius slider\nconst radiusSlider = this.shadowRoot.getElementById(\"radius-slider\");\nconst radiusValue = this.shadowRoot.getElementById(\"radius-value\");\n// Set initial value\nconst currentRadiusIndex = this.borderRadiusOptions.indexOf(\nthis.borderRadius);\nradiusSlider.value = currentRadiusIndex >= 0 ? currentRadiusIndex : 1;\nradiusValue.textContent = this.borderRadius;\nradiusSlider.addEventListener(\"input\", () => {\nconst newRadius = this.borderRadiusOptions[radiusSlider.value];\nradiusValue.textContent = newRadius;\nthemeContext.setBorderRadius(newRadius);\n});\n}\nrender() {\nconst theme = this.theme;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 16px 0;\npadding: 16px;\nborder-radius: ${this.borderRadius};\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#f9f9f9\" : \"#444\"};\ncolor: ${theme === \"light\" ? \"#333\" : \"#eee\"};\ntransition: all 0.3s;\nfont-family: system-ui, sans-serif;\n}\nh3 {\nmargin-top: 0;\ncolor: ${this.accentColor};\ntransition: color 0.3s;\n}\n.control-group {\nmargin-bottom: 16px;\n}\n.control-group label {\ndisplay: block;\nmargin-bottom: 8px;\nfont-weight: bold;\n}\n.theme-toggle {\ncursor: pointer;\nbackground-color: ${theme === \"light\" ? \"#333\" : \"#f5f5f5\"};\ncolor: ${theme === \"light\" ? \"#fff\" : \"#333\"};\nborder: none;\nborder-radius: ${this.borderRadius};\npadding: 8px 16px;\nfont-size: 14px;\ndisplay: flex;\nalign-items: center;\njustify-content: space-between;\nwidth: 140px;\ntransition: background-color 0.3s, color 0.3s;\n}\n.theme-toggle .icon {\ndisplay: inline-block;\nwidth: 16px;\nheight: 16px;\nborder-radius: 50%;\nbackground-color: ${theme === \"light\" ? \"#f5f5f5\" : \"#333\"};\nmargin-left: 8px;\nposition: relative;\n}\n.theme-toggle .icon::before {\ncontent: '';\nposition: absolute;\ntop: 3px;\nleft: ${theme === \"light\" ? \"3px\" : \"12px\"};\nwidth: 10px;\nheight: 10px;\nborder-radius: 50%;\nbackground-color: ${theme === \"light\" ? \"#333\" : \"#f5f5f5\"};\ntransition: left 0.3s;\n}\n.color-options {\ndisplay: flex;\ngap: 8px;\nflex-wrap: wrap;\n}\n.color-option {\nwidth: 32px;\nheight: 32px;\nborder-radius: 50%;\ncursor: pointer;\nposition: relative;\nborder: 2px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\ntransition: transform 0.2s;\n}\n.color-option:hover {\ntransform: scale(1.1);\n}\n.color-option.active {\nborder: 2px solid ${theme === \"light\" ? \"#333\" : \"#fff\"};\n}\n.color-option.active::after {\ncontent: \"✓\";\nposition: absolute;\ntop: 50%;\nleft: 50%;\ntransform: translate(-50%, -50%);\ncolor: ${this.isDarkColor(this.accentColor) ? \"#fff\" : \"#333\"};\nfont-size: 18px;\nfont-weight: bold;\n}\nselect, input[type=\"range\"] {\nwidth: 100%;\npadding: 6px;\nborder-radius: ${this.borderRadius};\nborder: 1px solid ${theme === \"light\" ? \"#ddd\" : \"#555\"};\nbackground-color: ${theme === \"light\" ? \"#fff\" : \"#333\"};\ncolor: ${theme === \"light\" ? \"#333\" : \"#fff\"};\n}\n.slider-container {\ndisplay: flex;\nalign-items: center;\ngap: 12px;\n}\n.slider-container input {\nflex: 1;\n}\n.value-display {\nmin-width: 40px;\ntext-align: center;\nfont-family: monospace;\npadding: 4px 8px;\nbackground-color: ${\ntheme === \"light\" ? \"rgba(0,0,0,0.05)\" : \"rgba(255,255,255,0.1)\"\n};\nborder-radius: ${this.borderRadius};\n}\n</style>\n<div>\n<h3>Theme Controller</h3>\n<div class=\"control-group\">\n<label>Theme Mode</label>\n<button id=\"theme-toggle\" class=\"theme-toggle\">\n${theme === \"light\" ? \"Light Mode\" : \"Dark Mode\"}\n<span class=\"icon\"></span>\n</button>\n</div>\n<div class=\"control-group\">\n<label>Accent Color</label>\n<div class=\"color-options\">\n${this.accentColors.\nmap(\ncolor => `\n<div\nclass=\"color-option ${\ncolor === this.accentColor ? \"active\" : \"\"\n}\"\nstyle=\"background-color: ${color};\"\ndata-color=\"${color}\">\n</div>\n`).\njoin(\"\")}\n</div>\n</div>\n<div class=\"control-group\">\n<label>Font Size</label>\n<select id=\"font-size-select\">\n${this.fontSizes.\nmap(\nsize => `\n<option value=\"${size}\" ${\nthis.fontSize === size ? \"selected\" : \"\"\n}>${size.charAt(0).toUpperCase() + size.slice(1)}</option>\n`).\njoin(\"\")}\n</select>\n</div>\n<div class=\"control-group\">\n<label>Border Radius</label>\n<div class=\"slider-container\">\n<input\nid=\"radius-slider\"\ntype=\"range\"\nmin=\"0\"\nmax=\"${this.borderRadiusOptions.length - 1}\"\nvalue=\"${this.borderRadiusOptions.indexOf(this.borderRadius)}\"\n/>\n<div id=\"radius-value\" class=\"value-display\">${\nthis.borderRadius\n}</div>\n</div>\n</div>\n</div>\n`;\nthis.addEventListeners();\n}\n// Helper to determine if a color is dark\nisDarkColor(color) {\n// Convert hex to RGB\nconst hex = color.replace(\"#\", \"\");\nconst r = parseInt(hex.substr(0, 2), 16);\nconst g = parseInt(hex.substr(2, 2), 16);\nconst b = parseInt(hex.substr(4, 2), 16);\n// Calculate brightness\nconst brightness = (r * 299 + g * 587 + b * 114) / 1000;\nreturn brightness < 128;\n}}\n// Register the theme controller component\ncustomElements.define(\"theme-controller\", ThemeController);\n// Register the components\ncustomElements.define(\"theme-provider\", ThemeProvider);\ncustomElements.define(\"theme-consumer\", ThemeConsumer);\ncustomElements.define(\"theme-toggler\", ThemeToggler);\nAdvanced State Management with Context on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nDeclarative Shadow DOM: Server-Side Renaissance\nOne of the most significant limitations of Web Components has long been server-side rendering capabilities. The Declarative Shadow DOM (DSD) is changing this landscape, enabling Web Components to be rendered on the server before reaching the browser.\nEnhance.dev is built around this approach. Here is the syntax. Note the attribute is shadowrootmode; the older shadowroot spelling from early drafts no longer creates a shadow root (checked in Chrome 156):\n<my-card>\n<template shadowrootmode=\"open\">\n<style>:host { display: block; }</style>\n<slot name=\"title\"></slot>\n</template>\n<h1 slot=\"title\">Hello from the server!</h1>\n</my-card>\nThe browser builds the shadow tree while parsing the HTML, so the component paints before any JavaScript runs.\nCheck out this interactive demo to see this pattern in action:\nHTML\n<my-card counter=\"1\">\n<template shadowrootmode=\"open\">\n<style>\n:host {\ndisplay: block;\nborder: 1px solid var(--border-color, #ccc);\nborder-radius: 8px;\npadding: 16px;\nfont-family: system-ui, sans-serif;\nbackground-color: var(--background-color, #fff);\ncolor: var(--text-color, #333);\n}\n.card-header {\nborder-bottom: 1px solid var(--divider-color, #eee);\npadding-bottom: 10px;\nmargin-bottom: 10px;\n}\n::slotted(h1) {\nmargin-top: 0;\ncolor: var(--text-color, #333);\n}\n.counter {\nmargin-top: 15px;\npadding-top: 15px;\nborder-top: 1px solid var(--divider-color, #eee);\n}\nbutton {\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nborder: 1px solid var(--border-color);\npadding: 5px 10px;\nmargin-right: 10px;\nborder-radius: 4px;\n}\n</style>\n<div class=\"card-header\">\n<slot name=\"title\">Default Title</slot>\n</div>\n<div class=\"card-body\">\n<slot>Default content</slot>\n</div>\n<div class=\"counter\">\n<p>Count: <span id=\"count\">0</span></p>\n<button id=\"decrement\">-</button>\n<button id=\"increment\">+</button>\n</div>\n</template>\n<h1 slot=\"title\">Hello from Declarative Shadow DOM</h1>\n<p>\nThis card demonstrates Declarative Shadow DOM for server-side rendering.\n</p>\n</my-card>\n<div class=\"explanation\">\n<h2>Benefits of Declarative Shadow DOM</h2>\n<p>\nDeclarative Shadow DOM provides significant advantages over imperative shadow DOM creation:\n</p>\n<ul>\n<li><strong>Server-Side Rendering:</strong> Components with encapsulated styling can be rendered on the server, improving initial page load performance</li>\n<li><strong>Zero Hydration Cost:</strong> No JavaScript is required to initialize the shadow DOM, making components immediately interactive</li>\n<li><strong>Progressive Enhancement:</strong> Components work even before JavaScript loads or if JavaScript fails</li>\n<li><strong>SEO Friendly:</strong> Content is available in the initial HTML, making it more accessible to search engines</li>\n<li><strong>Simplified Component Definition:</strong> No need for separate template files or string literals - the component structure is defined directly in HTML</li>\n</ul>\n<p>\nThe component above demonstrates these benefits while also implementing dark mode support,\nshowing how declarative shadow DOM can be both highly performant and user-friendly.\n</p>\n</div>\n<h2 class=\"code-header\">Declarative Shadow DOM Code:</h2>\n<pre><code>&lt;my-card counter=\"1\" &gt;\n&lt;template shadowrootmode=\"open\"&gt;\n&lt;style&gt;\n:host {\ndisplay: block;\nborder: 1px solid var(--border-color, #ccc);\nborder-radius: 8px;\npadding: 16px;\nfont-family: system-ui, sans-serif;\nbackground-color: var(--background-color, #fff);\ncolor: var(--text-color, #333);\n}\n.card-header {\nborder-bottom: 1px solid var(--divider-color, #eee);\npadding-bottom: 10px;\nmargin-bottom: 10px;\n}\n::slotted(h1) {\nmargin-top: 0;\ncolor: var(--text-color, #333);\n}\n.counter {\nmargin-top: 15px;\npadding-top: 15px;\nborder-top: 1px solid var(--divider-color, #eee);\n}\nbutton {\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nborder: 1px solid var(--border-color);\npadding: 5px 10px;\nmargin-right: 10px;\nborder-radius: 4px;\n}\n&lt;/style&gt;\n&lt;div class=\"card-header\"&gt;\n&lt;slot name=\"title\"&gt;Default Title&lt;/slot&gt;\n&lt;/div&gt;\n&lt;div class=\"card-body\"&gt;\n&lt;slot&gt;Default content&lt;/slot&gt;\n&lt;/div&gt;\n&lt;div class=\"counter\"&gt;\n&lt;p&gt;Count: &lt;span id=\"count\"&gt;0&lt;/span&gt;&lt;/p&gt;\n&lt;button id=\"decrement\"&gt;-&lt;/button&gt;\n&lt;button id=\"increment\"&gt;+&lt;/button&gt;\n&lt;/div&gt;\n&lt;/template&gt;\n&lt;h1 slot=\"title\"&gt;Hello from Declarative Shadow DOM&lt;/h1&gt;\n&lt;p&gt;\nThis card demonstrates Declarative Shadow DOM for server-side rendering.\n&lt;/p&gt;\n&lt;/my-card&gt;</code></pre>\n<div class=\"explanation\">\n<h2>Adding JavaScript Logic with Declarative Shadow DOM</h2>\n<p>\nWhile declarative shadow DOM provides the HTML structure and styling, you can still add JavaScript logic through custom elements:\n</p>\n<ul>\n<li><strong>Progressive Enhancement:</strong> The component renders with its structure and styling before JavaScript loads</li>\n<li><strong>Logic Attachment:</strong> JavaScript enhances the pre-rendered component with behavior after loading</li>\n<li><strong>Works either way:</strong> The custom element class works with both declaratively and imperatively created shadow DOM</li>\n<li><strong>Attribute Reactivity:</strong> Using attributes and the built-in lifecycle callbacks enables state management</li>\n</ul>\n<p>\nThis approach gives you the best of both worlds: instant rendering with declarative shadow DOM,\nplus fully interactive behavior once JavaScript executes.\n</p>\n</div>\nCSS\n:root {\ncolor-scheme: light dark;\n--background-color: #ffffff;\n--text-color: #333333;\n--border-color: #cccccc;\n--divider-color: #eeeeee;\n}\n@media (prefers-color-scheme: dark) {\n:root {\n--background-color: #222222;\n--text-color: #f0f0f0;\n--border-color: #555555;\n--divider-color: #444444;\n}\n}\nbody {\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nmargin: 20px;\n}\npre {\nbackground-color: var(--background-color);\nborder: 1px solid var(--border-color);\nborder-radius: 4px;\npadding: 15px;\noverflow: auto;\nmargin-top: 30px;\n}\ncode {\nfont-family: monospace;\ncolor: var(--text-color);\nwhite-space: pre;\n}\n.code-header {\nfont-weight: bold;\nmargin-bottom: 10px;\nmargin-top: 30px;\ncolor: var(--text-color);\n}\nJavaScript\n// Check and polyfill if necessary\nif (!HTMLTemplateElement.prototype.hasOwnProperty(\"shadowRootMode\")) {\ndocument.\nquerySelectorAll(\"template[shadowrootmode]\").\nforEach(template => {\nconst mode = template.getAttribute(\"shadowrootmode\");\nconst shadowRoot = template.parentNode.attachShadow({ mode });\nshadowRoot.appendChild(template.content);\ntemplate.remove();\n});\n}\n// Register the element with behavior\ncustomElements.define(\"my-card\", class extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['counter'];\n}\nconstructor() {\nsuper();\n// Get access to shadow DOM elements (works regardless of how shadow DOM was created)\nthis._setupShadowDOM();\n}\n_setupShadowDOM() {\n// If the shadow root already exists (from declarative shadow DOM)\n// we can use it directly, otherwise it would have been created by the polyfill\nconst shadowRoot = this.shadowRoot;\n// Get references to elements in the shadow DOM\nthis._countDisplay = shadowRoot.querySelector('#count');\nthis._incrementBtn = shadowRoot.querySelector('#increment');\nthis._decrementBtn = shadowRoot.querySelector('#decrement');\n// Add event listeners\nthis._incrementBtn.addEventListener('click', () => this._incrementCount());\nthis._decrementBtn.addEventListener('click', () => this._decrementCount());\n// Initialize with attribute value\nthis._updateCount(parseInt(this.getAttribute('counter') || '0'));\n}\nconnectedCallback() {\n// Make sure the count is updated with the latest attribute value\n// This handles the case where attributes might have changed before connection\nthis._updateCount(parseInt(this.getAttribute('counter') || '0'));\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'counter' && this._countDisplay) {\nthis._updateCount(parseInt(newValue || '0'));\n}\n}\n_incrementCount() {\nconst current = parseInt(this.getAttribute('counter') || '0');\nthis.setAttribute('counter', current + 1);\n}\n_decrementCount() {\nconst current = parseInt(this.getAttribute('counter') || '0');\nthis.setAttribute('counter', Math.max(0, current - 1));\n}\n_updateCount(value) {\nif (this._countDisplay) {\nthis._countDisplay.textContent = value;\n}\n}});\nDeclarative Shadow Dom on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nState Management Evolves\nState management in complex Web Components can quickly become unwieldy. The community has answered with a few small patterns, shown below.\nSignals libraries offer a React-inspired state model without the framework dependencies, while Lit State provides reactive state management specifically tailored to component systems. Most exciting is the Custom State Pseudo-Class, which now ships in browsers and lets you style components based on their internal state. A component adds a state with this.attachInternals().states.add('loading'), and CSS matches it with :state():\nmy-element:state(loading) {\nopacity: 0.5;\n}\nThese are patterns you can write today. None of them is a browser feature.\nHere is another interactive example that uses ElementInternals.states and the :state() pseudo-class, styling the component from the page and from inside its own shadow root.\nHTML\n<state-component></state-component>\nCSS\nbody {\nfont-family: system-ui, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\ntransition: background-color 0.3s, color 0.3s;\n}\n@media (prefers-color-scheme: dark) {\nbody {\nbackground-color: #121212;\ncolor: #e0e0e0;\n}\n}\n/* Custom states are styled from outside with :state(), with no attributes involved */\nstate-component:state(loading) {\nopacity: 0.5;\npointer-events: none;\n}\nstate-component:state(error) {\noutline: 3px solid #f44336;\noutline-offset: 4px;\nborder-radius: 8px;\n}\nJavaScript\nclass StateComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// ElementInternals.states is a CustomStateSet: whatever it holds,\n// CSS can match with :state(name), from outside or via :host(:state(name)).\nthis._internals = this.attachInternals();\n// Initial state\nthis.state = \"idle\";\nthis._internals.states.add(this.state);\n// One bound listener, so it can be removed again\nthis.handleColorSchemeChange = this.handleColorSchemeChange.bind(this);\n// Auto-reset timer reference\nthis.resetTimer = null;\n// Default auto-reset duration (ms)\nthis.resetDuration = 3000;\n// Check dark mode preference\nthis.darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn ['reset-duration', 'dark-mode'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'reset-duration' && newValue) {\nthis.resetDuration = parseInt(newValue, 10);\n} else if (name === 'dark-mode') {\nthis.darkMode = newValue === 'true';\nthis.render();\n}\n}\nconnectedCallback() {\n// Listen for system color scheme changes\nthis.colorSchemeMedia = window.matchMedia('(prefers-color-scheme: dark)');\nthis.colorSchemeMedia.addEventListener('change', this.handleColorSchemeChange);\n}\ndisconnectedCallback() {\n// Clean up timers when element is removed\nthis.clearResetTimer();\n// Remove event listener\nif (this.colorSchemeMedia) {\nthis.colorSchemeMedia.removeEventListener('change', this.handleColorSchemeChange);\n}\n}\nhandleColorSchemeChange(e) {\nthis.darkMode = e.matches;\nthis.render();\n}\nclearResetTimer() {\nif (this.resetTimer) {\nclearTimeout(this.resetTimer);\nthis.resetTimer = null;\n// Also clear the progress animation\nconst progressBar = this.shadowRoot.querySelector('.progress-bar');\nif (progressBar) {\nprogressBar.style.animation = 'none';\n}\n}\n}\nsetState(newState) {\nthis.state = newState;\nthis._internals.states.clear();\nthis._internals.states.add(newState);\n// Clear any existing timers\nthis.clearResetTimer();\n// Set automatic reset for non-idle states\nif (newState !== \"idle\") {\nthis.resetTimer = setTimeout(() => {\nthis.setState(\"idle\");\n}, this.resetDuration);\n// Update progress bar animation duration after render\nsetTimeout(() => {\nconst progressBar = this.shadowRoot.querySelector('.progress-bar');\nif (progressBar) {\nprogressBar.style.animation = `progress ${this.resetDuration / 1000}s linear forwards`;\n}\n}, 0);\n}\nthis.render();\n}\ntoggleDarkMode() {\nthis.darkMode = !this.darkMode;\nthis.setAttribute('dark-mode', this.darkMode);\nthis.render();\n}\nrender() {\nlet icon, message, ariaLive;\nswitch (this.state) {\ncase \"loading\":\nicon = \"⏳\";\nmessage = \"Loading...\";\nariaLive = \"polite\";\nbreak;\ncase \"success\":\nicon = \"✅\";\nmessage = \"Operation successful!\";\nariaLive = \"assertive\";\nbreak;\ncase \"error\":\nicon = \"❌\";\nmessage = \"An error occurred.\";\nariaLive = \"assertive\";\nbreak;\ndefault: // idle\nicon = \"⚪\";\nmessage = \"Component is idle. Click a button to change state.\";\nariaLive = \"off\";}\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\npadding: 20px;\nborder: 1px solid #ddd;\nborder-radius: 8px;\ntransition: all 0.3s;\nbackground-color: ${this.darkMode ? '#222' : '#fff'};\ncolor: ${this.darkMode ? '#e0e0e0' : '#333'};\n}\nh2 {\nmargin-top: 0;\ncolor: ${this.darkMode ? '#fff' : '#333'};\n}\n.container {\ndisplay: flex;\nalign-items: center;\nmargin-bottom: 20px;\n}\n.icon {\nfont-size: 32px;\nmargin-right: 16px;\n}\n:host(:state(success)) .message {\ncolor: #4caf50;\n}\n:host(:state(error)) .message {\ncolor: #f44336;\n}\n.message {\nfont-size: 18px;\n}\n.buttons {\ndisplay: grid;\ngrid-template-columns: repeat(4, 1fr);\ngap: 10px;\nmargin-bottom: 15px;\n}\nbutton {\npadding: 8px;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\ntransition: all 0.2s;\n}\nbutton:hover {\nfilter: brightness(0.9);\n}\nbutton:active {\ntransform: translateY(1px);\n}\nbutton:focus {\noutline: 2px solid #2196F3;\noutline-offset: 2px;\n}\nbutton.idle {\nbackground-color: ${this.darkMode ? '#555' : '#e0e0e0'};\ncolor: ${this.darkMode ? '#fff' : '#333'};\n}\nbutton.loading {\nbackground-color: #FFC107;\ncolor: ${this.darkMode ? '#000' : '#333'};\n}\nbutton.success {\nbackground-color: #4CAF50;\ncolor: white;\n}\nbutton.error {\nbackground-color: #f44336;\ncolor: white;\n}\nbutton.theme-toggle {\nbackground-color: ${this.darkMode ? '#777' : '#ddd'};\ncolor: ${this.darkMode ? '#fff' : '#333'};\nmargin-top: 10px;\n}\n.settings {\ndisplay: flex;\nflex-wrap: wrap;\nalign-items: center;\ngap: 12px;\nmargin-bottom: 15px;\n}\n.settings input {\nwidth: 4em;\n}\n.reset-info {\nfont-size: 14px;\nmargin: 10px 0;\ncolor: ${this.darkMode ? '#aaa' : '#666'};\ndisplay: ${this.state === \"idle\" ? \"none\" : \"block\"};\n}\n.progress-container {\nheight: 4px;\nwidth: 100%;\nbackground-color: ${this.darkMode ? '#444' : '#eee'};\nmargin-bottom: 15px;\nborder-radius: 2px;\noverflow: hidden;\ndisplay: ${this.state === \"idle\" ? \"none\" : \"block\"};\n}\n.progress-bar {\nheight: 100%;\nwidth: 100%;\nbackground-color: #2196F3;\ntransform-origin: left;\n}\n@keyframes progress {\n0% { transform: scaleX(1); }\n100% { transform: scaleX(0); }\n}\n.settings {\nmargin-top: 20px;\npadding-top: 15px;\nborder-top: 1px solid ${this.darkMode ? '#444' : '#eee'};\n}\n.config {\ndisplay: flex;\nalign-items: center;\ngap: 10px;\n}\n.config label {\nfont-size: 14px;\n}\n.config input {\nwidth: 70px;\nbackground-color: ${this.darkMode ? '#333' : '#fff'};\ncolor: ${this.darkMode ? '#fff' : '#333'};\nborder: 1px solid ${this.darkMode ? '#555' : '#ddd'};\npadding: 4px;\nborder-radius: 4px;\n}\nsmall {\ncolor: ${this.darkMode ? '#aaa' : '#666'};\n}\n</style>\n<div>\n<h2>Custom State Pseudo-Class Demo</h2>\n<p>This demonstrates how state changes can be exposed to CSS.</p>\n<div class=\"progress-container\" aria-hidden=\"true\">\n<div class=\"progress-bar\"></div>\n</div>\n<div class=\"reset-info\" aria-live=\"polite\">\nWill reset to idle in ${this.resetDuration / 1000} seconds\n</div>\n<div class=\"container\">\n<div class=\"icon\" aria-hidden=\"true\">${icon}</div>\n<div class=\"message\" aria-live=\"${ariaLive}\">${message}</div>\n</div>\n<div class=\"buttons\" role=\"group\" aria-label=\"State control buttons\">\n<button class=\"idle\" id=\"set-idle\">Idle</button>\n<button class=\"loading\" id=\"set-loading\">Loading</button>\n<button class=\"success\" id=\"set-success\">Success</button>\n<button class=\"error\" id=\"set-error\">Error</button>\n</div>\n<div class=\"settings\">\n<button id=\"toggle-theme\" type=\"button\">${this.darkMode ? 'Light' : 'Dark'} theme</button>\n<label>\nReset after\n<input id=\"reset-time\" type=\"number\" min=\"1\" max=\"30\" value=\"${this.resetDuration / 1000}\">\nseconds\n</label>\n</div>\n<p>\n<small>\nThis uses ElementInternals.states: the page styles\nstate-component:state(loading), and the component styles\n:host(:state(success)) from inside its shadow root.\n</small>\n</p>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.\nquerySelector(\"#set-idle\").\naddEventListener(\"click\", () => {\nthis.setState(\"idle\");\n});\nthis.shadowRoot.\nquerySelector(\"#set-loading\").\naddEventListener(\"click\", () => {\nthis.setState(\"loading\");\n});\nthis.shadowRoot.\nquerySelector(\"#set-success\").\naddEventListener(\"click\", () => {\nthis.setState(\"success\");\n});\nthis.shadowRoot.\nquerySelector(\"#set-error\").\naddEventListener(\"click\", () => {\nthis.setState(\"error\");\n});\n// Add theme toggle listener\nthis.shadowRoot.\nquerySelector(\"#toggle-theme\").\naddEventListener(\"click\", () => {\nthis.toggleDarkMode();\n});\n// Add reset duration input listener\nthis.shadowRoot.\nquerySelector(\"#reset-time\").\naddEventListener(\"change\", e => {\nconst seconds = parseInt(e.target.value, 10);\nif (seconds > 0) {\nthis.resetDuration = seconds * 1000;\n// If there's an active timer, restart it with new duration\nif (this.state !== \"idle\") {\nthis.setState(this.state);\n}\n}\n});\n}}\ncustomElements.define(\"state-component\", StateComponent);\nCustom State Pseudo-Class Demo on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nMicro-Frontends: The Enterprise Adoption\nA micro-frontend splits one page into parts that separate teams build and ship. Web components give each part a boundary that does not depend on a framework. Adobe's Spectrum Web Components and Microsoft's FAST are published as web components, so any of these parts can use them whatever framework it is written in.\nCommunity Momentum: Tools & Libraries\nLit and Tooling\nLit is the most widely used helper library in this post. It is a small base class that adds reactive properties and templates on top of custom elements. The Lit team also publishes experimental tools under the @lit-labs scope, such as @lit-labs/analyzer, which reads component source to describe it. Check Lit's own release notes for what is stable; this post makes no claims about future Lit versions.\nScoped Custom Element Registries: Solving Collisions\nThe registry of custom element names is global: one tag name can only point to one class for the whole page. Scoped custom element registries let a shadow root have its own registry. The snippet below ran correctly in Chrome 156. As of 26 October 2026 it also ships in Chrome and Edge 146 and Safari 26, but not in a stable Firefox, so feature-detect it (typeof CustomElementRegistry) and have a plan for the browsers without it. It does not work for customized built-in elements.\nconst registry = new CustomElementRegistry();\nregistry.define('my-element', MyElement);\nhost.attachShadow({ mode: 'open', customElementRegistry: registry });\ndocument.createElement('my-element', { customElementRegistry: registry });\nBefore browsers shipped it, the @webcomponents/scoped-custom-element-registry polyfill let teams try the pattern. Most apps never hit a tag-name clash, so this is a feature for the day two teams define <x-button> differently.\nBelow is an interactive example with two components that share one tag name. The demo checks for support first, so in a browser without scoped registries (Firefox, as of today) you will see a notice instead of the components.\nHTML\n<div class=\"demo-container\">\n<header>\n<h2>Scoped Custom Element Registries</h2>\n<button class=\"theme-toggle\" id=\"themeToggle\">Toggle Theme</button>\n</header>\n<p>\nThis demo uses scoped custom element registries, built into Chrome, Edge and Safari.\nThe same tag name can have completely different implementations in different component scopes.\n</p>\n<component-a></component-a>\n<component-b></component-b>\n<component-c></component-c>\n<div class=\"explanation\">\n<h3>About Scoped Custom Element Registries</h3>\n<p>\nScoped Custom Element Registries are part of the future of web components,\nallowing developers to:\n</p>\n<ul>\n<li>Avoid name collisions between components</li>\n<li>Use the same tag name for different implementations in different contexts</li>\n<li>Create truly encapsulated component systems</li>\n<li>Support easier component versioning</li>\n<li>Enable better component composition patterns</li>\n</ul>\n<p>\nEach component creates a <code>new CustomElementRegistry()</code>, defines its own\n<code>custom-button</code> in it, and passes it to\n<code>attachShadow({ customElementRegistry })</code>.\n</p>\n</div>\n</div>\nCSS\n:root {\n--primary-color: #6200ee;\n--secondary-color: #03dac6;\n--background-color: #ffffff;\n--text-color: #333333;\n}\n.dark-mode {\n--primary-color: #bb86fc;\n--secondary-color: #03dac6;\n--background-color: #121212;\n--text-color: #e0e0e0;\n}\nbody {\nfont-family: system-ui, sans-serif;\nbackground-color: var(--background-color);\ncolor: var(--text-color);\ntransition: background-color 0.3s ease;\n}\n.demo-container {\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\ncomponent-a,\ncomponent-b,\ncomponent-c {\ndisplay: block;\nmargin: 20px 0;\npadding: 20px;\nborder: 1px solid #ddd;\nborder-radius: 8px;\n}\nheader {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nmargin-bottom: 20px;\n}\n.theme-toggle {\nbackground-color: var(--primary-color);\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\n}\nJavaScript\n// Scoped Custom Element Registries: each component owns a CustomElementRegistry,\n// passes it to attachShadow(), and creates elements from it. Nothing is added to\n// the global customElements registry, so the same tag can mean different things.\nconst scopedRegistriesSupported = (() => {\ntry {\nnew CustomElementRegistry();\nreturn true;\n} catch {\nreturn false;\n}\n})();\nclass ScopedElementsMixin {\nconstructor() {\nthis.registry = new CustomElementRegistry();\n}\n// Define a custom element in this component's scope\ndefineScopedElement(tagName, constructor) {\nthis.registry.define(tagName, constructor);\nreturn constructor;\n}\n// Create an element from this scope's definitions\ncreateScopedElement(tagName) {\nreturn document.createElement(tagName, { customElementRegistry: this.registry });\n}\n// Check if a tag is defined in this scope\nhasScopedElement(tagName) {\nreturn this.registry.get(tagName) !== undefined;\n}\n}\n// Component A with its own registry\nclass ComponentA extends HTMLElement {\nconstructor() {\nsuper();\nthis.scopedElements = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.scopedElements.registry });\n// Define version A of the custom button\nclass CustomButtonA extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground-color: var(--primary-color);\ncolor: white;\nborder: none;\npadding: 10px 20px;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s ease;\n}\nbutton:hover {\nopacity: 0.9;\ntransform: translateY(-2px);\n}\n</style>\n<button>Component A Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Component A\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Register in the scoped registry\nthis.scopedElements.defineScopedElement(\"custom-button\", CustomButtonA);\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.card {\npadding: 16px;\nbackground-color: var(--background-color);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\ncolor: var(--primary-color);\n}\n.output {\nmargin-top: 10px;\nfont-style: italic;\ncolor: var(--primary-color);\n}\n</style>\n<div class=\"card\">\n<h3>Component A</h3>\n<p>This component has its own scoped definition of custom-button:</p>\n<div id=\"button-container\"></div>\n<div class=\"output\" id=\"output-a\"></div>\n</div>\n`;\n// Create the scoped element\nconst button = this.scopedElements.createScopedElement(\"custom-button\");\nthis.shadowRoot.querySelector(\"#button-container\").appendChild(button);\n// Listen for events from any component\ndocument.addEventListener(\"component-action\", e => {\nconst output = this.shadowRoot.querySelector(\"#output-a\");\noutput.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n});\n}}\n// Component B with its own registry\nclass ComponentB extends HTMLElement {\nconstructor() {\nsuper();\nthis.scopedElements = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.scopedElements.registry });\n// Define version B of the custom button\nclass CustomButtonB extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground-color: var(--secondary-color);\ncolor: black;\nborder: none;\npadding: 12px 24px;\nborder-radius: 50px;\nfont-size: 16px;\nfont-weight: bold;\ncursor: pointer;\ntransition: all 0.3s ease;\n}\nbutton:hover {\nbox-shadow: 0 4px 8px rgba(0,0,0,0.2);\n}\n</style>\n<button>Component B Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Component B\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Register in the scoped registry\nthis.scopedElements.defineScopedElement(\"custom-button\", CustomButtonB);\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.card {\npadding: 16px;\nbackground-color: var(--background-color);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\ncolor: var(--secondary-color);\n}\n.output {\nmargin-top: 10px;\nfont-style: italic;\ncolor: var(--secondary-color);\n}\n</style>\n<div class=\"card\">\n<h3>Component B</h3>\n<p>This component has its own scoped definition of custom-button:</p>\n<div id=\"button-container\"></div>\n<div class=\"output\" id=\"output-b\"></div>\n</div>\n`;\n// Create the scoped element\nconst button = this.scopedElements.createScopedElement(\"custom-button\");\nthis.shadowRoot.querySelector(\"#button-container\").appendChild(button);\n// Listen for events from any component\ndocument.addEventListener(\"component-action\", e => {\nconst output = this.shadowRoot.querySelector(\"#output-b\");\noutput.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n});\n}}\n// Component C with nested scoped elements\nclass ComponentC extends HTMLElement {\nconstructor() {\nsuper();\nthis.scopedElements = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.scopedElements.registry });\n// Define version C of the custom button\nclass CustomButtonC extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\ncolor: white;\nborder: none;\npadding: 10px 20px;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\nbox-shadow: 0 2px 5px rgba(0,0,0,0.2);\ntransition: all 0.3s ease;\n}\nbutton:hover {\ntransform: scale(1.05);\n}\n</style>\n<button>Component C Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Component C\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Define a nested component with its own registry\nclass NestedComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.nestedRegistry = new ScopedElementsMixin();\nthis.attachShadow({ mode: \"open\", customElementRegistry: this.nestedRegistry.registry });\n// Define a unique version of custom-button for nested component\nclass NestedButton extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<style>\nbutton {\nbackground-color: transparent;\ncolor: var(--text-color);\nborder: 2px solid currentColor;\npadding: 8px 16px;\nborder-radius: 4px;\nfont-size: 14px;\ncursor: pointer;\n}\n</style>\n<button>Nested Button</button>\n`;\nconst button = this.querySelector(\"button\");\nbutton.addEventListener(\"click\", () => {\nconst event = new CustomEvent(\"nested-action\", {\nbubbles: true,\ncomposed: true });\nthis.dispatchEvent(event);\n});\n}}\n// Register nested button in its own registry\nthis.nestedRegistry.defineScopedElement(\"custom-button\", NestedButton);\n// Render nested component\nthis.shadowRoot.innerHTML = `\n<style>\n.nested {\nmargin: 10px;\npadding: 10px;\nborder: 1px dashed var(--text-color);\nborder-radius: 4px;\n}\nh4 {\nmargin-top: 0;\nfont-size: 14px;\n}\n</style>\n<div class=\"nested\">\n<h4>Nested Component Scope</h4>\n<p>This nested component has its own scoped registry:</p>\n<div id=\"nested-button-container\"></div>\n</div>\n`;\n// Create and append the nested button\nconst nestedButton = this.nestedRegistry.createScopedElement(\n\"custom-button\");\nthis.shadowRoot.\nquerySelector(\"#nested-button-container\").\nappendChild(nestedButton);\n// Listen for clicks on the nested button\nthis.shadowRoot.addEventListener(\"nested-action\", () => {\nconst event = new CustomEvent(\"component-action\", {\nbubbles: true,\ncomposed: true,\ndetail: { source: \"Nested Component\", action: \"clicked\" } });\nthis.dispatchEvent(event);\n});\n}}\n// Register components in C's scope\nthis.scopedElements.defineScopedElement(\"custom-button\", CustomButtonC);\nthis.scopedElements.defineScopedElement(\"nested-component\", NestedComponent);\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.card {\npadding: 16px;\nbackground-color: var(--background-color);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\nbackground: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n-webkit-background-clip: text;\n-webkit-text-fill-color: transparent;\n}\n.component-registry {\nmargin-top: 15px;\nfont-size: 14px;\n}\n.output {\nmargin-top: 10px;\nfont-style: italic;\nbackground: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n-webkit-background-clip: text;\n-webkit-text-fill-color: transparent;\nfont-weight: bold;\n}\n</style>\n<div class=\"card\">\n<h3>Component C (Nested Components)</h3>\n<p>This component demonstrates nested scoped registries:</p>\n<div id=\"button-container\"></div>\n<div id=\"nested-container\"></div>\n<div class=\"component-registry\">\n<p>Registered elements in this scope:\n<code>custom-button</code>, <code>nested-component</code>\n</p>\n</div>\n<div class=\"output\" id=\"output-c\"></div>\n</div>\n`;\n// Create the scoped elements\nconst button = this.scopedElements.createScopedElement(\"custom-button\");\nconst nested = this.scopedElements.createScopedElement(\"nested-component\");\nthis.shadowRoot.querySelector(\"#button-container\").appendChild(button);\nthis.shadowRoot.querySelector(\"#nested-container\").appendChild(nested);\n// Listen for events from any component\ndocument.addEventListener(\"component-action\", e => {\nconst output = this.shadowRoot.querySelector(\"#output-c\");\noutput.textContent = `Event received: ${e.detail.source} ${e.detail.action}`;\n});\n}}\n// Register the components\nif (scopedRegistriesSupported) {\ncustomElements.define(\"component-a\", ComponentA);\ncustomElements.define(\"component-b\", ComponentB);\ncustomElements.define(\"component-c\", ComponentC);\n} else {\nconst notice = document.createElement(\"p\");\nnotice.className = \"explanation\";\nnotice.textContent = \"This browser does not support scoped custom element registries yet, so the components cannot be shown.\";\ndocument.querySelector(\"component-a\").before(notice);\n}\n// Theme toggle functionality\nconst themeToggleButton = document.getElementById(\"themeToggle\");\n// Function to update button text based on current theme\nfunction updateThemeButtonText() {\nthemeToggleButton.textContent = document.body.classList.contains(\"dark-mode\") ?\n\"Light Theme\" :\n\"Dark Theme\";\n}\n// Check user's system preference for dark mode\nfunction initializeThemeBasedOnPreference() {\n// Check if user prefers dark mode\nconst prefersDarkMode = window.matchMedia(\"(prefers-color-scheme: dark)\").\nmatches;\n// Set initial theme based on preference\nif (prefersDarkMode) {\ndocument.body.classList.add(\"dark-mode\");\n}\n// Update button text to reflect initial state\nupdateThemeButtonText();\n}\n// Initialize theme when page loads\ninitializeThemeBasedOnPreference();\n// Theme toggle button click handler\nthemeToggleButton.addEventListener(\"click\", () => {\ndocument.body.classList.toggle(\"dark-mode\");\nupdateThemeButtonText();\n});\nScoped Custom Element Registries on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nTesting Gets Easier\nWeb Test Runner can run component tests in real browsers, and Storybook has a web-components renderer that can read a component's description from a Custom Elements Manifest. Both reduce the need for framework-specific test setups.\nOn the Horizon: Features to Watch\nManual Slot Assignment\nNormally the browser fills slots by matching slot=\"name\" attributes. Manual assignment lets script decide instead. It ran in Chrome 156, and it is Baseline widely available (all three engines since March 2023).\nconst root = host.attachShadow({ mode: 'open', slotAssignment: 'manual' });\nconst slot = document.createElement('slot');\nroot.append(slot);\nslot.assign(host.querySelector('h1')); // this child now fills the slot\nYou may see an element.attachSlot('header') call in older articles. That method does not exist (it is undefined in Chrome 156). Use slotAssignment: 'manual' and slot.assign().\nQuestion: Which of these is NOT a real browser API?\nslot.assign(node) with slotAssignment: 'manual'\nnew CustomElementRegistry()\nelement.attachSlot('header')\ninternals.states.add('loading')\nAnswer: 3. element.attachSlot('header')\nExplanation: attachSlot does not exist. Manual slotting is slotAssignment: 'manual' plus slot.assign(); scoped registries and custom states are real and ran in Chrome 156.\n@scope: Scoped Rules, Not Shadow DOM\n@scope limits where a CSS rule applies in a normal stylesheet. It became Baseline newly available in March 2026:\n@scope (.card) {\nh1 { color: red; } /* Only matches h1 elements inside .card */\n}\nIt works in the light DOM and does not create or cross a shadow boundary (in the test, an h1 inside .card turned red and an h1 outside stayed black). Styling across a shadow boundary is done with CSS custom properties, ::part() and ::slotted(), all of which ship today.\nQuestion: What does @scope (.card) { h1 { color: red; } } do?\nCreates a shadow root on every .card\nApplies the rule only to h1 elements inside .card\nLets outside CSS style the inside of any shadow root\nAnswer: 2. Applies the rule only to h1 elements inside .card\nExplanation: @scope is a cascade feature for the normal DOM. Shadow roots are created with attachShadow().\nWebAssembly Integration\nWebAssembly is compiled code that runs in the browser. A custom element can call into it, for example to do heavy number crunching while keeping a plain HTML interface. Tools such as wasm-bindgen let Rust code talk to JavaScript. This is a combination of existing features, not a new web component standard.\nCheck out a simulation of how we can bring WebAssembly into a web component below:\nHTML\n<button id=\"dark-mode-toggle\" class=\"dark-mode-toggle\">🌙</button>\n<h1>WebAssembly Component Integration</h1>\n<p>\nThis component demonstrates how WebAssembly can be integrated with Web\nComponents for high-performance computation.\n</p>\n<wasm-component initial-value=\"5\" wasm-url=\"math_ops.wasm\"></wasm-component>\n<div style=\"margin-top: 30px\">\n<h3>Component API Usage</h3>\n<button id=\"external-set\">Set Value to 42</button>\n<button id=\"external-get\">Get Current Value</button>\n</div>\nCSS\nbody {\nfont-family: system-ui, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\ntransition: background-color 0.3s, color 0.3s;\n}\nbody.dark-mode {\nbackground-color: #121212;\ncolor: #e0e0e0;\n}\n.dark-mode-toggle {\nposition: absolute;\ntop: 20px;\nright: 20px;\nbackground: none;\nborder: none;\ncursor: pointer;\nfont-size: 24px;\n}\nh1,\nh3 {\ntransition: color 0.3s;\n}\nbody.dark-mode h1,\nbody.dark-mode h3 {\ncolor: #e0e0e0;\n}\nJavaScript\n// Dark mode functionality\nfunction initDarkMode() {\n// Check for user preference\nconst prefersDarkMode =\nwindow.matchMedia &&\nwindow.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n// Set initial mode based on preference\nif (prefersDarkMode) {\ndocument.body.classList.add(\"dark-mode\");\n}\n// Add toggle listener\ndocument.getElementById(\"dark-mode-toggle\").addEventListener(\"click\", () => {\ndocument.body.classList.toggle(\"dark-mode\");\nupdateDarkModeIcon();\n});\nupdateDarkModeIcon();\n}\nfunction updateDarkModeIcon() {\nconst isDarkMode = document.body.classList.contains(\"dark-mode\");\ndocument.getElementById(\"dark-mode-toggle\").textContent = isDarkMode\n? \"☀️\"\n: \"🌙\";\n}\nclass WasmComponent extends HTMLElement {\nstatic observedAttributes = [\"initial-value\", \"wasm-url\"];\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.counter = 0;\nthis.isLoading = true;\nthis.hasError = false;\nthis.errorMessage = \"\";\nthis.wasmModule = null;\nthis.MAX_VALUE = 1.9767639272283756e154; // Maximum allowed value\n// Check if dark mode is active when component is created\nthis.isDarkMode = document.body.classList.contains(\"dark-mode\");\nthis.render();\n}\nconnectedCallback() {\n// Listen for dark mode changes on the body\nthis._darkModeObserver = new MutationObserver((mutations) => {\nmutations.forEach((mutation) => {\nif (mutation.attributeName === \"class\") {\nthis.isDarkMode = document.body.classList.contains(\"dark-mode\");\nthis.render();\n}\n});\n});\nthis._darkModeObserver.observe(document.body, { attributes: true });\n// Get initial values from attributes\nif (this.hasAttribute(\"initial-value\")) {\nthis.counter = parseInt(this.getAttribute(\"initial-value\"), 10) || 0;\nthis.updateDisplay();\n}\n// Load the WASM module\nconst wasmUrl = this.getAttribute(\"wasm-url\") || \"math_ops.wasm\";\nthis.loadWasmModule(wasmUrl);\n}\ndisconnectedCallback() {\n// Clean up observer when component is removed\nif (this._darkModeObserver) {\nthis._darkModeObserver.disconnect();\n}\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"initial-value\" && this.shadowRoot) {\nthis.counter = parseInt(newValue, 10) || 0;\nthis.updateDisplay();\n}\nif (name === \"wasm-url\" && oldValue !== newValue && this.isConnected) {\nthis.isLoading = true;\nthis.hasError = false;\nthis.render();\nthis.loadWasmModule(newValue);\n}\n}\nasync loadWasmModule(url) {\ntry {\nthis.isLoading = true;\nthis.render();\nawait new Promise((resolve) => setTimeout(resolve, 1000));\nthis.wasmModule = {\nexports: {\nincrement: (value) => {\nconst result = value + 1;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\ndecrement: (value) => value - 1,\nsquare: (value) => {\nconst result = value * value;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\ncube: (value) => {\nconst result = value * value * value;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\ndouble: (value) => {\nconst result = value * 2;\nreturn result > this.MAX_VALUE ? this.MAX_VALUE : result;\n},\nhalf: (value) => value / 2,\nfactorial: (value) => {\nif (value <= 1) return 1;\nlet result = 1;\nfor (let i = 2; i <= value; i++) {\nresult *= i;\nif (result > this.MAX_VALUE) return this.MAX_VALUE;\n}\nreturn result;\n},\nisPrime: (value) => {\nif (value <= 1) return 0;\nif (value <= 3) return 1;\nif (value % 2 === 0 || value % 3 === 0) return 0;\nlet i = 5;\nwhile (i * i <= value) {\nif (value % i === 0 || value % (i + 2) === 0) return 0;\ni += 6;\n}\nreturn 1;\n}\n}\n};\nthis.isLoading = false;\nthis.hasError = false;\nthis.dispatchEvent(\nnew CustomEvent(\"wasm-loaded\", {\nbubbles: true,\ncomposed: true,\ndetail: { success: true }\n})\n);\n} catch (error) {\nconsole.error(\"Failed to load WASM module:\", error);\nthis.isLoading = false;\nthis.hasError = true;\nthis.errorMessage = error.message || \"Failed to load WebAssembly module\";\nthis.dispatchEvent(\nnew CustomEvent(\"wasm-error\", {\nbubbles: true,\ncomposed: true,\ndetail: { error }\n})\n);\n}\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\npadding: 20px;\nborder: 1px solid ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\nborder-radius: 8px;\nbackground-color: ${this.isDarkMode ? \"#1e1e2f\" : \"#f8f5ff\"};\ncolor: ${this.isDarkMode ? \"#e0e0e0\" : \"inherit\"};\ntransition: background-color 0.3s, border-color 0.3s, color 0.3s;\n}\nh2 {\nmargin-top: 0;\ncolor: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\ntransition: color 0.3s;\n}\n.display {\nfont-size: 48px;\ntext-align: center;\nmargin: 20px 0;\nfont-weight: bold;\ncolor: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\ntransition: color 0.3s;\n}\n.buttons {\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: center;\ngap: 10px;\nmargin-bottom: 15px;\n}\nbutton {\nbackground-color: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\ncolor: ${this.isDarkMode ? \"#121212\" : \"white\"};\nborder: none;\npadding: 10px 20px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s, color 0.2s;\n}\nbutton:hover {\nbackground-color: ${this.isDarkMode ? \"#b39ddb\" : \"#5e35b1\"};\n}\nbutton:disabled {\nbackground-color: ${this.isDarkMode ? \"#4a4a4a\" : \"#b39ddb\"};\ncursor: not-allowed;\n}\n.loading {\ntext-align: center;\npadding: 20px;\ncolor: ${this.isDarkMode ? \"#9575cd\" : \"#673AB7\"};\n}\n.loading::after {\ncontent: \"...\";\nanimation: dots 1.5s infinite;\n}\n.error {\nbackground-color: ${this.isDarkMode ? \"#3c1f1f\" : \"#ffebee\"};\nborder: 1px solid ${this.isDarkMode ? \"#b71c1c\" : \"#f44336\"};\ncolor: ${this.isDarkMode ? \"#f48fb1\" : \"#d32f2f\"};\npadding: 10px;\nborder-radius: 4px;\nmargin: 10px 0;\n}\n.note {\nmargin-top: 20px;\nfont-size: 14px;\ncolor: ${this.isDarkMode ? \"#aaa\" : \"#666\"};\nfont-style: italic;\n}\n.prime-indicator {\nfont-size: 14px;\nmargin-top: 5px;\ntext-align: center;\nfont-weight: bold;\n}\n.prime-yes {\ncolor: ${this.isDarkMode ? \"#81c784\" : \"#4caf50\"};\n}\n.prime-no {\ncolor: ${this.isDarkMode ? \"#e57373\" : \"#f44336\"};\n}\n@keyframes dots {\n0%, 20% { content: \".\"; }\n40% { content: \"..\"; }\n60%, 100% { content: \"...\"; }\n}\n</style>\n<div>\n<h2>Advanced WebAssembly Component Demo</h2>\n<p>This demonstrates how a Web Component integrates with WebAssembly.</p>\n${\nthis.isLoading\n? `\n<div class=\"loading\">Loading WebAssembly module</div>\n`\n: this.hasError\n? `\n<div class=\"error\">\n<strong>Error:</strong> ${\nthis.errorMessage || \"Failed to load WebAssembly module\"\n}\n</div>\n`\n: `\n<div class=\"display\">${this.counter}</div>\n<div class=\"prime-indicator ${\nthis.wasmModule?.exports.isPrime(this.counter)\n? \"prime-yes\"\n: \"prime-no\"\n}\">\n${\nthis.wasmModule?.exports.isPrime(this.counter)\n? \"This is a prime number!\"\n: this.counter > 1\n? \"This is not a prime number\"\n: \"\"\n}\n</div>\n<div class=\"buttons\">\n<button id=\"decrement\" ?disabled=\"${\nthis.isLoading\n}\">Decrement</button>\n<button id=\"increment\" ?disabled=\"${\nthis.isLoading\n}\">Increment</button>\n<button id=\"double\" ?disabled=\"${\nthis.isLoading\n}\">Double</button>\n<button id=\"half\" ?disabled=\"${\nthis.isLoading\n}\">Half</button>\n<button id=\"square\" ?disabled=\"${\nthis.isLoading\n}\">Square</button>\n<button id=\"cube\" ?disabled=\"${\nthis.isLoading\n}\">Cube</button>\n<button id=\"factorial\" ?disabled=\"${\nthis.isLoading || this.counter > 12\n}\">Factorial</button>\n<button id=\"reset\" ?disabled=\"${\nthis.isLoading\n}\">Reset</button>\n</div>\n`\n}\n<p class=\"note\">\n${\nthis.isLoading\n? \"Loading WebAssembly module...\"\n: this.hasError\n? \"Failed to load WebAssembly module. Check console for details.\"\n: \"Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.\"\n}\n</p>\n</div>\n`;\nif (!this.isLoading && !this.hasError) {\nthis.shadowRoot.querySelector(\"#increment\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.increment(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#decrement\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.decrement(this.counter);\nthis.updateDisplay();\n});\nthis.shadowRoot.querySelector(\"#square\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.square(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#cube\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.cube(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#double\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.double(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n});\nthis.shadowRoot.querySelector(\"#half\").addEventListener(\"click\", () => {\nthis.counter = this.wasmModule.exports.half(this.counter);\nthis.updateDisplay();\n});\nthis.shadowRoot.querySelector(\"#factorial\").addEventListener(\"click\", () => {\nif (this.counter <= 12) {\nthis.counter = this.wasmModule.exports.factorial(this.counter);\nthis.updateDisplay();\nthis.checkMaxValueReached();\n}\n});\nthis.shadowRoot.querySelector(\"#reset\").addEventListener(\"click\", () => {\nthis.counter = 0;\nthis.updateDisplay();\n});\n}\n}\ncheckMaxValueReached() {\nif (this.counter >= this.MAX_VALUE) {\nconsole.warn(\"Maximum value reached\");\nconst note = this.shadowRoot.querySelector(\".note\");\nif (note) {\nnote.innerHTML =\n'<strong style=\"color: #ff9800;\">Warning: Maximum value reached. Some operations may be limited.</strong>';\nsetTimeout(() => {\nnote.innerHTML =\n\"Note: This simulates WASM integration. In a production environment, these operations would be performed by actual WebAssembly code.\";\n}, 3000);\n}\n}\n}\nupdateDisplay() {\nif (this.shadowRoot.querySelector(\".display\")) {\nlet displayText = this.counter;\nif (this.counter > 1e15) {\ndisplayText = this.counter.toExponential(2);\n}\nthis.shadowRoot.querySelector(\".display\").textContent = displayText;\nconst primeIndicator = this.shadowRoot.querySelector(\".prime-indicator\");\nif (primeIndicator) {\nconst isPrime = this.wasmModule.exports.isPrime(this.counter);\nprimeIndicator.className = `prime-indicator ${\nisPrime ? \"prime-yes\" : \"prime-no\"\n}`;\nprimeIndicator.textContent = isPrime\n? \"This is a prime number!\"\n: this.counter > 1\n? \"This is not a prime number\"\n: \"\";\n}\nconst factorialBtn = this.shadowRoot.querySelector(\"#factorial\");\nif (factorialBtn) {\nfactorialBtn.disabled = this.counter > 12;\n}\n}\nthis.dispatchEvent(\nnew CustomEvent(\"value-changed\", {\nbubbles: true,\ncomposed: true,\ndetail: { value: this.counter }\n})\n);\n}\ngetValue() {\nreturn this.counter;\n}\nsetValue(value) {\nconst parsedValue = parseInt(value, 10) || 0;\nthis.counter = parsedValue > this.MAX_VALUE ? this.MAX_VALUE : parsedValue;\nthis.updateDisplay();\nthis.checkMaxValueReached();\n}\n}\ncustomElements.define(\"wasm-component\", WasmComponent);\ndocument.addEventListener(\"DOMContentLoaded\", initDarkMode);\ndocument.getElementById(\"external-set\").addEventListener(\"click\", () => {\nconst component = document.querySelector(\"wasm-component\");\ncomponent.setValue(42);\n});\ndocument.getElementById(\"external-get\").addEventListener(\"click\", () => {\nconst component = document.querySelector(\"wasm-component\");\nalert(`Current value: ${component.getValue()}`);\n});\ndocument\n.querySelector(\"wasm-component\")\n.addEventListener(\"value-changed\", (e) => {\nconsole.log(\"Value changed:\", e.detail.value);\n});\ndocument\n.querySelector(\"wasm-component\")\n.addEventListener(\"wasm-loaded\", (e) => {\nconsole.log(\"WASM module loaded successfully\");\n});\nWebAssembly Component Demo on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/).\nChallenges That Remain\nFrameworks. React passes unknown props to custom elements as attributes in version 18 and earlier (checked with 18.3.1 in the post on frameworks (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/)). React 19 changed this and is not covered here.\nAccessibility. Custom elements do not get built-in semantics. ElementInternals.role and the ARIA properties let a component declare its role; internals.role = 'button' worked in Chrome 156. A property named accessibleRole does not exist.\nLearning. Documentation and examples spread across libraries remain the main barrier for newcomers.\nConclusion\nYou can now tell which Web Component features are safe to use and which are not. Use ::part(), :state(), Declarative Shadow DOM (shadowrootmode), manual slotting, constructable stylesheets and custom events today. Check @scope against your oldest supported browser, and treat scoped registries as a feature for Chrome, Edge and Safari until Firefox ships it. When an article names a method you have not seen, search for it on MDN or run it in your console before building on it, as attachSlot shows.\nFor a next step, build a small component that uses one of the \"ships today\" rows from the table above. To follow standards work, the W3C Web Components Community Group (https://www.w3.org/community/webcomponents/) publishes its discussions.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/the-future-of-web-components/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-04-20T04:00:25Z",
      "id": "https://mnixblog.netlify.app/the-future-of-web-components/",
      "image": "https://mnixblog.netlify.app/images/the-future-of-web-components/cover-v4.jpg",
      "summary": "Which Web Component features you can use today, which work in only some browsers, and which are still proposals, with a dated status table.",
      "tags": [
        "WebAssembly",
        "Web Components",
        "CSS",
        "Frameworks"
      ],
      "title": "The Future of Web Components",
      "url": "https://mnixblog.netlify.app/the-future-of-web-components/"
    },
    {
      "content_html": "<p>Pass a JavaScript object to a web component from React, and the component receives the text <code>[object Object]</code>. That is the whole problem this post solves: frameworks and web components pass data in different ways, and a few small habits bridge them.</p>\n<p>When a framework such as React meets a custom tag like <code>&lt;user-badge&gt;</code>, data and events can get lost on the way across. This post shows where, and how to stop it.</p>\n<p><strong>Prerequisites in two lines:</strong> a custom element can take small settings as HTML attributes (always strings) and richer data as JavaScript properties (<code>el.data = {...}</code>). It talks back with custom events that you listen for with <code>addEventListener</code>.</p>\n<p>React is covered in full and Vue is the second example. Angular and Svelte follow the same two ideas (property for data, <code>addEventListener</code> for events), so their sections are optional reading.</p>\n<h2 id=\"why-framework-integration-matters\">Why Framework Integration Matters<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#why-framework-integration-matters\" aria-label=\"Link to this section\">#</a></h2>\n<p>Web Components were designed to be universal, but the reality of modern web development means they must coexist with frameworks that have their own component models and lifecycles. Successful integration requires understanding both worlds and the boundaries between them.</p>\n<p>Here is the failure, so you can recognize it. In React 18 (checked with 18.3.1), this element receives a string, not your object:</p>\n<pre><code class=\"language-jsx\">&lt;user-badge user={{ name: 'Ada' }} /&gt;\n// the DOM now has user=\"[object Object]\", and the component's `user` property is untouched\n</code></pre>\n<p>React 18 writes unknown props on custom elements as attributes, and an attribute can only hold text. A function prop such as <code>onChartUpdate={fn}</code> is also not turned into an event listener for a custom event. React 19 changed how props reach custom elements and is not covered here, so check its release notes if you use it.</p>\n<p>What makes integration challenging?</p>\n<ul>\n<li>\n<p><strong>Different Data Flow Models</strong>: React's unidirectional data flow differs fundamentally from the property-based approach of Web Components.</p>\n</li>\n<li>\n<p><strong>Event Handling Discrepancies</strong>: Each framework has its own event system that must be reconciled with the DOM event model.</p>\n</li>\n<li>\n<p><strong>Lifecycle Management</strong>: Coordinating component lifecycle events between frameworks and Web Components requires careful orchestration.</p>\n</li>\n</ul>\n<p>Let's explore how to overcome these challenges with practical, reusable patterns.</p>\n<h2 id=\"using-web-components-with-modern-frameworks\">Using Web Components with Modern Frameworks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#using-web-components-with-modern-frameworks\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"react-integration-strategies\">React Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#react-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>React's declarative approach to UI challenges direct integration with the imperative DOM APIs of Web Components. Here are advanced patterns to bridge this gap:</p>\n<h4 id=\"1-the-ref-pattern-direct-dom-access\">1. The Ref Pattern: Direct DOM Access<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-the-ref-pattern-direct-dom-access\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">function MyReactComponent() {\n  const wcRef = useRef(null);\n\n  useEffect(() =&gt; {\n    if (wcRef.current) {\n      // Direct property assignment for complex data\n      wcRef.current.complexData = { key: \"value\" };\n\n      // Event handling with proper cleanup\n      const el = wcRef.current;\n      const handleEvent = (e) =&gt; console.log(e.detail);\n      el.addEventListener('custom-event', handleEvent);\n\n      return () =&gt; {\n        el.removeEventListener('custom-event', handleEvent);\n      };\n    }\n  }, []);\n\n  return &lt;my-web-component ref={wcRef} string-prop=\"This works\" /&gt;;\n}\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p>React passes string attributes directly, but complex data requires refs</p>\n</li>\n<li>\n<p>Event handling needs manual listeners with proper cleanup</p>\n</li>\n<li>\n<p>Ref access enables imperative methods on Web Components</p>\n</li>\n</ul>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A page script wants a component to receive an object. Which call gives the component the object itself?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>el.setAttribute('data', obj)</code></li>\n<li><code>el.data = obj</code></li>\n<li><code>el.getAttribute('data')</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>el.data = obj</code></p>\n<p><code>setAttribute</code> converts its value to a string (<code>\"[object Object]\"</code> for a plain object). Assigning a property keeps the object as it is.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: Hand React's data to the element</strong></p>\n<p><code>&lt;user-badge&gt;</code> has a <code>user</code> property, but the React wrapper never sets it, so the preview says \"Hello, nobody yet\".</p>\n<p><strong>Expected result:</strong> after you finish the TODO the preview says \"Hello, Ada\".</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Set a property through a ref</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class UserBadge extends HTMLElement {\n  constructor() { super(); this.attachShadow({ mode: &#39;open&#39; }); }\n  set user(value) { this._user = value; this.render(); }\n  get user() { return this._user; }\n  connectedCallback() { this.render(); }\n  render() {\n    const name = this._user ? this._user.name : &#39;nobody yet&#39;;\n    this.shadowRoot.innerHTML = &#39;&lt;p&gt;Hello, &#39; + name + &#39;&lt;/p&gt;&#39;;\n  }\n}\ncustomElements.define(&#39;user-badge&#39;, UserBadge);\n\nconst { useRef, useEffect, createElement: h } = React;\n\nfunction Badge({ user }) {\n  const ref = useRef(null);\n  // TODO: in a useEffect, set ref.current.user = user (run again when user changes)\n  return h(&#39;user-badge&#39;, { ref });\n}\n\nReactDOM.createRoot(document.getElementById(&#39;root&#39;)).render(\n  h(Badge, { user: { name: &#39;Ada&#39; } })\n);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p><code>useEffect(() =&gt; { ref.current.user = user; }, [user]);</code> goes where the TODO is.</p>\n</aside>\n<h4 id=\"2-the-wrapper-pattern-creating-react-friendly-components\">2. The Wrapper Pattern: Creating React-Friendly Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#2-the-wrapper-pattern-creating-react-friendly-components\" aria-label=\"Link to this section\">#</a></h4>\n<p>React developers expect components with React-like patterns. Create wrapper components that abstract Web Component peculiarities:</p>\n<pre><code class=\"language-jsx\">// Wrapper that handles property mapping and events\nfunction EnhancedWebComponent({ data, onCustomEvent, children }) {\n  const ref = useRef(null);\n\n  useEffect(() =&gt; {\n    const element = ref.current;\n    if (element) {\n      // Set complex data\n      element.data = data;\n\n      // Handle events\n      const eventHandler = (e) =&gt; onCustomEvent(e.detail);\n      element.addEventListener('custom-event', eventHandler);\n\n      return () =&gt; element.removeEventListener('custom-event', eventHandler);\n    }\n  }, [data, onCustomEvent]);\n\n  return (\n    &lt;my-web-component ref={ref}&gt;\n      {children}\n    &lt;/my-web-component&gt;\n  );\n}\n\n// Usage feels like a normal React component\nfunction App() {\n  return (\n    &lt;EnhancedWebComponent \n      data={{complex: \"data\"}} \n      onCustomEvent={handleEvent}\n    &gt;\n      &lt;p&gt;Child content&lt;/p&gt;\n    &lt;/EnhancedWebComponent&gt;\n  );\n}\n</code></pre>\n<p><strong>Interactive Demo:</strong> Try the React integration patterns yourself</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">React + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;root&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.demo-container {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n.examples {\n  display: flex;\n  flex-direction: column;\n  gap: 20px;\n}\n\n.example {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 16px;\n  background-color: #fff;\n}\n\nh2 {\n  color: #2c3e50;\n  margin-top: 0;\n}\n\nh3 {\n  color: #2c3e50;\n  margin-top: 0;\n}\n\n.button-group {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 8px;\n}\n\n.react-button {\n  background-color: #61dafb;\n  color: #000;\n  border: none;\n  border-radius: 4px;\n  padding: 8px 16px;\n  cursor: pointer;\n  font-size: 16px;\n}\n\n.events {\n  margin-top: 16px;\n  padding: 8px;\n  background-color: #f8f9fa;\n  border-radius: 4px;\n  font-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">const { useState, useRef, useEffect } = React;\n\n// First, define the Web Component\n// First, define the Web Component\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._count = 0;\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&#39;count&#39;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;count&#39;) {\n      this._count = parseInt(newValue || &#39;0&#39;, 10);\n      this.render();\n    }\n  }\n\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;counter-changed&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n  }\n\n  increment() {\n    this._count++;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;counter-changed&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: sans-serif;\n          border: 2px solid #3498db;\n          border-radius: 8px;\n          padding: 16px;\n          margin: 8px 0;\n          background-color: #f8f9fa;\n        }\n        .counter {\n          font-size: 24px;\n          margin-bottom: 12px;\n        }\n        button {\n          background-color: #3498db;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 16px;\n          cursor: pointer;\n          font-size: 16px;\n          transition: background-color 0.2s;\n        }\n        button:hover {\n          background-color: #2980b9;\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n      &lt;button id=&quot;increment&quot;&gt;Increment&lt;/button&gt;\n    `;\n\n    this.shadowRoot.querySelector(&#39;#increment&#39;).addEventListener(&#39;click&#39;, () =&gt; this.increment());\n  }\n\n  disconnectedCallback() {\n    const btn = this.shadowRoot.querySelector(&#39;#increment&#39;);\n    if (btn) {\n      btn.removeEventListener(&#39;click&#39;, () =&gt; this.increment());\n    }\n  }}\n\n\n// Register the web component\nif (!customElements.get(&#39;simple-counter&#39;)) {\n  customElements.define(&#39;simple-counter&#39;, SimpleCounter);\n}\n\n// React Components using React.createElement for CodePen compatibility\nfunction DirectInteractionExample() {\n  const [events, setEvents] = React.useState([]);\n  const counterRef = React.useRef(null);\n\n  React.useEffect(() =&gt; {\n    if (counterRef.current) {\n      // Set initial count\n      counterRef.current.count = 5;\n\n      // Set up event listener\n      const handleCounterChanged = e =&gt; {\n        setEvents(prev =&gt; [...prev, `Counter changed: ${e.detail.count}`]);\n      };\n\n      counterRef.current.addEventListener(&#39;counter-changed&#39;, handleCounterChanged);\n\n      // Clean up\n      return () =&gt; {\n        if (counterRef.current) {\n          counterRef.current.removeEventListener(&#39;counter-changed&#39;, handleCounterChanged);\n        }\n      };\n    }\n  }, []);\n\n  // Match Angular&#39;s button actions\n  const handleIncrement = () =&gt; {\n    if (counterRef.current) {\n      counterRef.current.increment();\n    }\n  };\n\n  const handleDecrement = () =&gt; {\n    if (counterRef.current &amp;&amp; counterRef.current.count &gt; 0) {\n      counterRef.current.count = counterRef.current.count - 1;\n      setEvents(prev =&gt; [...prev, `Counter decremented to: ${counterRef.current.count}`]);\n    }\n  };\n\n  const handleReset = () =&gt; {\n    if (counterRef.current) {\n      counterRef.current.count = 0;\n      setEvents(prev =&gt; [...prev, &#39;Counter reset to 0&#39;]);\n    }\n  };\n\n  const handleDoubleValue = () =&gt; {\n    if (counterRef.current) {\n      const currentValue = counterRef.current.count;\n      counterRef.current.count = currentValue * 2;\n      setEvents(prev =&gt; [...prev, `Doubled value to: ${counterRef.current.count}`]);\n    }\n  };\n\n  return React.createElement(&#39;div&#39;, { className: &#39;example&#39; },\n  React.createElement(&#39;h3&#39;, null, &#39;Using React with Web Components&#39;),\n  React.createElement(&#39;p&#39;, null, &#39;Direct component manipulation using refs&#39;),\n\n  React.createElement(&#39;simple-counter&#39;, { ref: counterRef }),\n\n  React.createElement(&#39;div&#39;, { className: &#39;button-group&#39; },\n  React.createElement(&#39;button&#39;, {\n    onClick: handleIncrement,\n    className: &#39;react-button&#39; },\n  &#39;Increment&#39;),\n\n  React.createElement(&#39;button&#39;, {\n    onClick: handleDecrement,\n    className: &#39;react-button&#39; },\n  &#39;Decrement&#39;),\n\n  React.createElement(&#39;button&#39;, {\n    onClick: handleReset,\n    className: &#39;react-button&#39; },\n  &#39;Reset&#39;)),\n\n\n  React.createElement(&#39;div&#39;, { className: &#39;button-group&#39; },\n  React.createElement(&#39;button&#39;, {\n    onClick: handleDoubleValue,\n    className: &#39;react-button&#39; },\n  &#39;Double Value&#39;)),\n\n\n  React.createElement(&#39;div&#39;, { className: &#39;events&#39; },\n  React.createElement(&#39;strong&#39;, null, &#39;Events:&#39;),\n  events.map((event, i) =&gt; React.createElement(&#39;div&#39;, { key: i }, event))));\n\n\n}\n\n// Main Demo Component\nfunction ReactWebComponentDemo() {\n  return React.createElement(&#39;div&#39;, { className: &#39;demo-container&#39; },\n  React.createElement(&#39;h2&#39;, null, &#39;React + Web Components Integration&#39;),\n  React.createElement(&#39;p&#39;, null,\n  &#39;This example demonstrates how React can interact with Web Components through refs and direct DOM manipulation.&#39;),\n\n\n  React.createElement(DirectInteractionExample));\n\n}\n\n// Render the demo\nReactDOM.render(React.createElement(ReactWebComponentDemo), document.getElementById(&#39;root&#39;));\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"angular-integration-strategies\">Angular Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#angular-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Angular has built-in support for Web Components through its <code>@angular/elements</code> package, but deeper integration still requires specific techniques:</p>\n<h4 id=\"1-element-binding-with-custom-directives\">1. Element Binding with Custom Directives<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-element-binding-with-custom-directives\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-typescript\">@Directive({\n  selector: 'my-web-component'\n})\nexport class WebComponentDirective implements OnInit, OnChanges, OnDestroy {\n  @Input() complexData: any;\n  @Output() customEvent = new EventEmitter&lt;any&gt;();\n\n  constructor(private el: ElementRef) {}\n\n  ngOnInit() {\n    this.el.nativeElement.addEventListener('custom-event', \n      this.handleCustomEvent.bind(this));\n  }\n\n  ngOnChanges(changes: SimpleChanges) {\n    if (changes.complexData) {\n      this.el.nativeElement.complexData = this.complexData;\n    }\n  }\n\n  ngOnDestroy() {\n    this.el.nativeElement.removeEventListener('custom-event', \n      this.handleCustomEvent.bind(this));\n  }\n\n  private handleCustomEvent(event: CustomEvent) {\n    this.customEvent.emit(event.detail);\n  }\n}\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p>Angular directives provide clean property and event binding</p>\n</li>\n<li>\n<p>OnChanges lifecycle hook updates properties efficiently</p>\n</li>\n<li>\n<p>ElementRef gives direct access to the native element</p>\n</li>\n</ul>\n<p><strong>Interactive Demo:</strong> Explore Angular integrations</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Angular + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div ng-app=&quot;webComponentDemo&quot; ng-controller=&quot;MainController&quot;&gt;\n\t&lt;div class=&quot;demo-container&quot;&gt;\n\t\t&lt;h2&gt;Angular + Web Components Integration&lt;/h2&gt;\n\t\t&lt;p&gt;\n\t\t\tThis example demonstrates how Angular can work with Web Components through directive bindings\n\t\t\tand direct DOM access.\n\t\t&lt;/p&gt;\n\n\t\t&lt;div class=&quot;examples&quot;&gt;\n\t\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t\t&lt;h3&gt;Using Angular Directives&lt;/h3&gt;\n\t\t\t\t&lt;p&gt;Binding component state to Angular&#39;s data model&lt;/p&gt;\n\n\t\t\t\t&lt;simple-counter count=&quot;counterValue&quot; on-counter-changed=&quot;handleCounterEvent($event)&quot;&gt;\n\t\t\t\t&lt;/simple-counter&gt;\n\n\t\t\t\t&lt;button class=&quot;angular-button&quot; ng-click=&quot;incrementCounter()&quot;&gt;\n\t\t\t\t\tAngular: Increment Counter\n\t\t\t\t&lt;/button&gt;\n\n\t\t\t\t&lt;div class=&quot;events&quot;&gt;\n\t\t\t\t\t&lt;strong&gt;Current count in Angular: {{ counterValue }}&lt;/strong&gt;\n\t\t\t\t\t&lt;div ng-repeat=&quot;event in events track by $index&quot;&gt;\n\t\t\t\t\t\t{{ event }}\n\t\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\n\t\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t\t&lt;h3&gt;Direct DOM Access&lt;/h3&gt;\n\t\t\t\t&lt;p&gt;Using element references to directly call component methods&lt;/p&gt;\n\n\t\t\t\t&lt;simple-counter id=&quot;direct-counter&quot;&gt;&lt;/simple-counter&gt;\n\n\t\t\t\t&lt;button class=&quot;angular-button&quot; ng-click=&quot;callDirectMethod()&quot;&gt;\n\t\t\t\t\tCall Component Method Directly\n\t\t\t\t&lt;/button&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.demo-container {\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tOxygen, Ubuntu, Cantarell, &quot;Open Sans&quot;, &quot;Helvetica Neue&quot;, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n}\n\n.examples {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 20px;\n}\n\n.example {\n\tborder: 1px solid #ddd;\n\tborder-radius: 8px;\n\tpadding: 16px;\n\tbackground-color: #fff;\n}\n\nh2 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\nh3 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\n.angular-button {\n\tbackground-color: #dd0031;\n\tcolor: white;\n\tborder: none;\n\tborder-radius: 4px;\n\tpadding: 8px 16px;\n\tmargin-top: 8px;\n\tcursor: pointer;\n\tfont-size: 16px;\n}\n\n.events {\n\tmargin-top: 16px;\n\tpadding: 8px;\n\tbackground-color: #f8f9fa;\n\tborder-radius: 4px;\n\tfont-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our Web Component\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this._count = 0;\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&quot;count&quot;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &quot;count&quot;) {\n      this._count = parseInt(newValue || &quot;0&quot;, 10);\n      this.render();\n    }\n  }\n\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.render();\n\n    // When set programmatically, update the attribute to keep in sync\n    this.setAttribute(&quot;count&quot;, value);\n  }\n\n  increment() {\n    this._count++;\n    this.render();\n    this.dispatchEvent(\n    new CustomEvent(&quot;counter-changed&quot;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n\n\n    // Update the attribute to keep in sync with the property\n    this.setAttribute(&quot;count&quot;, this._count);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          :host {\n            display: block;\n            font-family: sans-serif;\n            border: 2px solid #dd0031;\n            border-radius: 8px;\n            padding: 16px;\n            margin: 8px 0;\n            background-color: #f8f9fa;\n          }\n          .counter {\n            font-size: 24px;\n            margin-bottom: 12px;\n          }\n          button {\n            background-color: #dd0031;\n            color: white;\n            border: none;\n            border-radius: 4px;\n            padding: 8px 16px;\n            cursor: pointer;\n            font-size: 16px;\n            transition: background-color 0.2s;\n          }\n          button:hover {\n            background-color: #a50023;\n          }\n        &lt;/style&gt;\n        &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n        &lt;button id=&quot;increment&quot;&gt;Increment from Inside&lt;/button&gt;\n      `;\n\n    this.shadowRoot.\n    querySelector(&quot;#increment&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.increment());\n  }\n\n  disconnectedCallback() {\n    const button = this.shadowRoot.querySelector(&quot;#increment&quot;);\n    if (button) {\n      button.removeEventListener(&quot;click&quot;, () =&gt; this.increment());\n    }\n  }}\n\n\n// Register our Web Component\nif (!customElements.get(&quot;simple-counter&quot;)) {\n  customElements.define(&quot;simple-counter&quot;, SimpleCounter);\n}\n\n// Angular app\nangular.\nmodule(&quot;webComponentDemo&quot;, []).\ncontroller(&quot;MainController&quot;, [\n&quot;$scope&quot;,\n&quot;$timeout&quot;,\nfunction ($scope, $timeout) {\n  // Initialize data\n  $scope.counterValue = 5;\n  $scope.events = [];\n\n  // Event handler for counter changes\n  $scope.handleCounterEvent = function (event) {\n    // Use $timeout to ensure Angular knows about the update\n    $timeout(function () {\n      $scope.counterValue = event.detail.count;\n      $scope.events.push(&quot;Counter changed: &quot; + event.detail.count);\n    });\n  };\n\n  // Method to increment the counter from Angular\n  $scope.incrementCounter = function () {\n    $scope.counterValue++;\n    // Angular will update the attribute automatically\n  };\n\n  // Method to call the component directly\n  $scope.callDirectMethod = function () {\n    var counter = document.getElementById(&quot;direct-counter&quot;);\n    if (counter) {\n      counter.increment();\n\n      // Add to the events list\n      $timeout(function () {\n        $scope.events.push(&quot;Called increment() method directly&quot;);\n      });\n    }\n  };\n\n  // Setup event handling for the direct counter\n  $timeout(function () {\n    var counter = document.getElementById(&quot;direct-counter&quot;);\n    if (counter) {\n      // Set initial value\n      counter.count = 10;\n\n      // Listen for events\n      counter.addEventListener(&quot;counter-changed&quot;, function (event) {\n        $timeout(function () {\n          $scope.events.push(&quot;Direct counter changed: &quot; + event.detail.count);\n        });\n      });\n    }\n  });\n\n  // We need to create a directive to bind our Web Component properly\n  // This is done to demonstrate the concept, but in CodePen it&#39;s simpler to\n  // do the binding manually as we&#39;ve done above\n}])\n\n\n// Add a simple directive to handle attribute binding\n.directive(&quot;simpleCounter&quot;, function () {\n  return {\n    restrict: &quot;E&quot;,\n    scope: {\n      count: &quot;=?&quot;, // Two-way binding (optional: the direct counter has no count attribute)\n      onCounterChanged: &quot;&amp;&quot; // Function binding\n    },\n    link: function (scope, element, attrs) {\n      // Watch for changes to the &#39;count&#39; attribute\n      scope.$watch(&quot;count&quot;, function (newValue) {\n        if (newValue !== undefined) {\n          element[0].count = newValue;\n        }\n      });\n\n      // Listen for counter-changed events\n      element[0].addEventListener(&quot;counter-changed&quot;, function (event) {\n        scope.$evalAsync(function () {\n          scope.count = event.detail.count;\n          scope.onCounterChanged({ $event: event });\n        });\n      });\n    } };\n\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"vue-integration-strategies\">Vue Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#vue-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Vue offers excellent built-in support for Web Components with its attribute and event binding syntax:</p>\n<pre><code class=\"language-html\">&lt;template&gt;\n  &lt;my-web-component \n    :complex-prop=\"myData\" \n    @custom-event=\"handleEvent\"\n  /&gt;\n&lt;/template&gt;\n\n&lt;script&gt;\nexport default {\n  data() {\n    return {\n      myData: { key: 'value' }\n    }\n  },\n  methods: {\n    handleEvent(event) {\n      console.log(event.detail);\n    }\n  },\n  mounted() {\n    // For cases where direct property access is needed\n    this.$refs.myComponent.directMethod();\n  }\n}\n&lt;/script&gt;\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p>Vue's <code>:prop</code> syntax works with kebab-case attributes</p>\n</li>\n<li>\n<p>Native event handling with <code>@event-name</code></p>\n</li>\n<li>\n<p>Vue's reactivity system naturally updates Web Component properties</p>\n</li>\n</ul>\n<p><strong>Interactive Demo:</strong> Explore Vue integrations</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Vue + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;app&quot;&gt;\n\t&lt;h2&gt;Vue + Web Components Integration&lt;/h2&gt;\n\t&lt;p&gt;\n\t\tVue offers excellent support for Web Components with its built-in directive syntax.\n\t\tThis demo shows how properties and events work.\n\t&lt;/p&gt;\n\n\t&lt;div class=&quot;demo-container&quot;&gt;\n\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t&lt;h3&gt;Basic Integration&lt;/h3&gt;\n\t\t\t&lt;p&gt;Vue&#39;s declarative syntax makes Web Component integration straightforward&lt;/p&gt;\n\n\t\t\t&lt;simple-counter :count=&quot;counterData&quot; @counter-changed=&quot;handleCounterEvent&quot;&gt;&lt;/simple-counter&gt;\n\n\t\t\t&lt;button class=&quot;vue-button&quot; @click=&quot;incrementCounter&quot;&gt;\n\t\t\t\tIncrement from Vue\n\t\t\t&lt;/button&gt;\n\n\t\t\t&lt;div class=&quot;events&quot;&gt;\n\t\t\t\t&lt;strong&gt;Current count in Vue data: {{ counterData }}&lt;/strong&gt;\n\t\t\t\t&lt;div v-for=&quot;(event, index) in events&quot; :key=&quot;index&quot;&gt;\n\t\t\t\t\t{{ event }}\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\n\t\t&lt;div class=&quot;example&quot;&gt;\n\t\t\t&lt;h3&gt;Using $refs for Direct Access&lt;/h3&gt;\n\t\t\t&lt;p&gt;When needed, Vue can access Web Components directly with $refs&lt;/p&gt;\n\n\t\t\t&lt;simple-counter ref=&quot;directCounter&quot;&gt;&lt;/simple-counter&gt;\n\n\t\t\t&lt;button class=&quot;vue-button&quot; @click=&quot;callComponentMethod&quot;&gt;\n\t\t\t\tCall component method directly\n\t\t\t&lt;/button&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tOxygen, Ubuntu, Cantarell, &quot;Open Sans&quot;, &quot;Helvetica Neue&quot;, sans-serif;\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n}\n\n.demo-container {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 20px;\n}\n\n.example {\n\tborder: 1px solid #ddd;\n\tborder-radius: 8px;\n\tpadding: 16px;\n\tbackground-color: #fff;\n}\n\nh2 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\nh3 {\n\tcolor: #2c3e50;\n\tmargin-top: 0;\n}\n\n.vue-button {\n\tbackground-color: #42b883;\n\tcolor: white;\n\tborder: none;\n\tborder-radius: 4px;\n\tpadding: 8px 16px;\n\tmargin-top: 8px;\n\tcursor: pointer;\n\tfont-size: 16px;\n}\n\n.events {\n\tmargin-top: 16px;\n\tpadding: 8px;\n\tbackground-color: #f8f9fa;\n\tborder-radius: 4px;\n\tfont-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define Web Component\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this._count = 0;\n    this.render();\n  }\n\n  static get observedAttributes() {\n    return [&quot;count&quot;];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &quot;count&quot;) {\n      this._count = parseInt(newValue || &quot;0&quot;, 10);\n      this.render();\n    }\n  }\n\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.render();\n  }\n\n  increment() {\n    this._count++;\n    this.render();\n    this.dispatchEvent(\n    new CustomEvent(&quot;counter-changed&quot;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              font-family: sans-serif;\n              border: 2px solid #42b883;\n              border-radius: 8px;\n              padding: 16px;\n              margin: 8px 0;\n              background-color: #f8f9fa;\n            }\n            .counter {\n              font-size: 24px;\n              margin-bottom: 12px;\n            }\n            button {\n              background-color: #42b883;\n              color: white;\n              border: none;\n              border-radius: 4px;\n              padding: 8px 16px;\n              cursor: pointer;\n              font-size: 16px;\n              transition: background-color 0.2s;\n            }\n            button:hover {\n              background-color: #33a06f;\n            }\n          &lt;/style&gt;\n          &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n          &lt;button id=&quot;increment&quot;&gt;Increment&lt;/button&gt;\n        `;\n\n    this.shadowRoot.\n    querySelector(&quot;#increment&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.increment());\n  }\n\n  disconnectedCallback() {\n    const button = this.shadowRoot.querySelector(&quot;#increment&quot;);\n    if (button) {\n      button.removeEventListener(&quot;click&quot;, () =&gt; this.increment());\n    }\n  }}\n\n\n// Register the Web Component\nif (!customElements.get(&quot;simple-counter&quot;)) {\n  customElements.define(&quot;simple-counter&quot;, SimpleCounter);\n}\n\n// Vue App\nconst app = Vue.createApp({\n  data() {\n    return {\n      counterData: 5,\n      events: [] };\n\n  },\n  methods: {\n    handleCounterEvent(event) {\n      this.counterData = event.detail.count;\n      this.events.push(`Counter changed: ${event.detail.count}`);\n    },\n    incrementCounter() {\n      this.counterData++;\n      this.$nextTick(() =&gt; {\n        // Vue&#39;s reactivity will update the attribute on the Web Component\n      });\n    },\n    callComponentMethod() {\n      // Direct method call via ref\n      this.$refs.directCounter.increment();\n      this.events.push(&quot;Called increment() method directly&quot;);\n    } },\n\n  mounted() {\n    // Initial setup if needed\n    this.$refs.directCounter.count = 10;\n  } });\n\n\napp.mount(&quot;#app&quot;);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"svelte-integration-strategies\">Svelte Integration Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#svelte-integration-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Svelte treats Web Components as first-class citizens and makes integration particularly straightforward:</p>\n<pre><code class=\"language-html\">&lt;script&gt;\n  import { onMount } from 'svelte';\n\n  let el;\n  let myData = { key: 'value' };\n\n  // Reactive assignments automatically update the Web Component\n  $: if (el) {\n    el.complexData = myData;\n  }\n\n  function handleEvent(event) {\n    console.log(event.detail);\n  }\n\n  onMount(() =&gt; {\n    // If needed, do direct manipulation here\n    el.addEventListener('special-event', specialHandler);\n\n    return () =&gt; {\n      el.removeEventListener('special-event', specialHandler);\n    }\n  });\n&lt;/script&gt;\n\n&lt;my-web-component \n  bind:this={el} \n  on:custom-event={handleEvent}\n/&gt;\n</code></pre>\n<p><strong>Key Insights:</strong></p>\n<ul>\n<li>\n<p><code>bind:this</code> provides direct access to the element</p>\n</li>\n<li>\n<p>Svelte's reactivity automatically updates properties</p>\n</li>\n<li>\n<p>Svelte's event handling works natively with Web Component events</p>\n</li>\n</ul>\n<p><strong>Interactive Demo:</strong> Explore Svelte integration Techniques</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Svelte + Web Components Integration</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;!-- The Svelte component below (App) is compiled in your browser and mounted here. --&gt;\n&lt;div id=&quot;app&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 20px;\n  background-color: #f8f9fa;\n}\n\n.demo-container {\n  margin: 20px 0;\n}\n\n.example {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 16px;\n  margin-bottom: 20px;\n  background-color: #fff;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.05);\n}\n\nh2,\nh3 {\n  color: #2c3e50;\n  margin-top: 0;\n}\n\n.button-group {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 8px;\n}\n\n.component-button,\n.svelte-button {\n  color: white;\n  border: none;\n  border-radius: 4px;\n  padding: 8px 16px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.2s;\n}\n\n.component-button { background-color: #3498db; }\n.component-button:hover { background-color: #2980b9; }\n.svelte-button { background-color: #ff3e00; }\n.svelte-button:hover { background-color: #d03400; }\n\n.events {\n  margin-top: 16px;\n  padding: 12px;\n  background-color: #f8f9fa;\n  border-radius: 4px;\n  font-size: 14px;\n  border: 1px solid #e9ecef;\n  max-height: 200px;\n  overflow-y: auto;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// 1. A framework-agnostic Web Component: a counter with a `count` property,\n//    an `increment()` method, and a `counter-changed` event.\nclass SimpleCounter extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._count = 0;\n    this.render();\n  }\n  static get observedAttributes() {\n    return [&#39;count&#39;];\n  }\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;count&#39;) this.count = parseInt(newValue || &#39;0&#39;, 10);\n  }\n  get count() {\n    return this._count;\n  }\n  set count(value) {\n    if (value === this._count) return;\n    this._count = value;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;counter-changed&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count }\n    }));\n  }\n  increment() {\n    this.count = this._count + 1;\n  }\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: sans-serif;\n          border: 2px solid #ff3e00;\n          border-radius: 8px;\n          padding: 16px;\n          margin: 8px 0;\n          background-color: #f8f9fa;\n        }\n        .counter { font-size: 24px; margin-bottom: 12px; }\n        button {\n          background-color: #ff3e00;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 16px;\n          cursor: pointer;\n          font-size: 16px;\n        }\n        button:hover { background-color: #d03400; }\n      &lt;/style&gt;\n      &lt;div class=&quot;counter&quot;&gt;Count: ${this._count}&lt;/div&gt;\n      &lt;button id=&quot;increment&quot;&gt;Increment from Inside&lt;/button&gt;\n    `;\n    this.shadowRoot.querySelector(&#39;#increment&#39;).addEventListener(&#39;click&#39;, () =&gt; this.increment());\n  }\n}\nif (!customElements.get(&#39;simple-counter&#39;)) {\n  customElements.define(&#39;simple-counter&#39;, SimpleCounter);\n}\n\n// 2. A real Svelte component that uses it: bind:this for direct access,\n//    a reactive statement to push store values in, and on:counter-changed to listen.\nconst App = `\n&lt;script&gt;\n  import { onMount } from &#39;svelte&#39;;\n  import { writable } from &#39;svelte/store&#39;;\n\n  const count = writable(5);\n  const events = writable([]);\n  let counter;\n\n  // Reactive statement: whenever $count changes, update the Web Component\n  $: if (counter &amp;&amp; counter.count !== $count) {\n    counter.count = $count;\n  }\n\n  function log(message) {\n    events.update((list) =&gt; [message, ...list].slice(0, 8));\n  }\n\n  // The Web Component&#39;s own event, handled with Svelte&#39;s on: directive\n  function handleCounterChanged(event) {\n    count.set(event.detail.count);\n    log(&#39;counter-changed: &#39; + event.detail.count);\n  }\n\n  onMount(() =&gt; {\n    log(&#39;Mounted; bind:this gave us &lt;&#39; + counter.localName + &#39;&gt;&#39;);\n  });\n&lt;/script&gt;\n\n&lt;div class=&quot;demo-container&quot;&gt;\n  &lt;h2&gt;Svelte + Web Components Integration&lt;/h2&gt;\n  &lt;p&gt;A real Svelte component driving a Web Component with bind:this, a store, and its events&lt;/p&gt;\n\n  &lt;div class=&quot;example&quot;&gt;\n    &lt;h3&gt;Dual Approach Integration&lt;/h3&gt;\n    &lt;p&gt;Compare calling the Web Component&#39;s own method with updating a Svelte store&lt;/p&gt;\n\n    &lt;simple-counter bind:this={counter} on:counter-changed={handleCounterChanged}&gt;&lt;/simple-counter&gt;\n\n    &lt;div class=&quot;button-group&quot;&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; counter.increment()}&gt;Increment (Component)&lt;/button&gt;\n      &lt;button class=&quot;svelte-button&quot; on:click={() =&gt; count.update((n) =&gt; n + 1)}&gt;Increment (Svelte Store)&lt;/button&gt;\n    &lt;/div&gt;\n\n    &lt;div class=&quot;button-group&quot;&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; count.update((n) =&gt; Math.max(0, n - 1))}&gt;Decrement&lt;/button&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; count.set(0)}&gt;Reset&lt;/button&gt;\n      &lt;button class=&quot;component-button&quot; on:click={() =&gt; count.update((n) =&gt; n * 2)}&gt;Double Value&lt;/button&gt;\n    &lt;/div&gt;\n\n    &lt;div class=&quot;events&quot;&gt;\n      &lt;strong&gt;Current count in the Svelte store: {$count}&lt;/strong&gt;\n      {#each $events as event, i (i)}\n        &lt;div&gt;{event}&lt;/div&gt;\n      {/each}\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n`;\n\n// 3. Compile the component in the browser (as the Svelte REPL does) and mount it.\n//    `svelte` is the compiler; `SvelteRuntime` holds the modules compiled code imports.\nfunction load(source) {\n  const { js } = svelte.compile(source, { name: &#39;App&#39;, generate: &#39;client&#39; });\n  const code = js.code\n    .replace(/^import\\s+[&#39;&quot;][^&#39;&quot;]+[&#39;&quot;];?$/gm, &#39;&#39;)\n    .replace(/^import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s+[&#39;&quot;]([^&#39;&quot;]+)[&#39;&quot;];?$/gm, &#39;const $1 = runtime(&quot;$2&quot;);&#39;)\n    .replace(/^import\\s+\\{([^}]*)\\}\\s+from\\s+[&#39;&quot;]([^&#39;&quot;]+)[&#39;&quot;];?$/gm,\n      (_, names, from) =&gt; `const {${names.replace(/\\s+as\\s+/g, &#39;: &#39;)}} = runtime(&quot;${from}&quot;);`)\n    .replace(/^export default /m, &#39;const Component = &#39;);\n  const runtime = (name) =&gt; {\n    if (!(name in SvelteRuntime.modules)) throw new Error(`This sandbox can only import Svelte&#39;s own modules, not &quot;${name}&quot;.`);\n    return SvelteRuntime.modules[name];\n  };\n  return new Function(&#39;runtime&#39;, `${code}\\nreturn Component;`)(runtime);\n}\n\ntry {\n  const { mount } = SvelteRuntime.modules.svelte;\n  mount(load(App), { target: document.getElementById(&#39;app&#39;) });\n  console.log(`Compiled and mounted with Svelte ${svelte.VERSION}`);\n} catch (error) {\n  console.error(error.message);\n}\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h2 id=\"performance-optimization-techniques\">Performance Optimization Techniques<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#performance-optimization-techniques\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"lazy-loading-strategies\">Lazy-Loading Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#lazy-loading-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Loading Web Components only when needed can significantly improve initial page load performance:</p>\n<h4 id=\"dynamic-import-pattern\">Dynamic Import Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#dynamic-import-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">// Only load when needed\nconst loadComponent = async () =&gt; {\n  const { MyComponent } = await import('./my-component.js');\n  customElements.define('my-component', MyComponent);\n}\n\n// Call on demand or when component is about to be needed\nbutton.addEventListener('click', loadComponent);\n</code></pre>\n<h4 id=\"intersection-observer-pattern\">Intersection Observer Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#intersection-observer-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">// Setup observer to load component when scrolled into view\nconst observer = new IntersectionObserver((entries) =&gt; {\n  entries.forEach(entry =&gt; {\n    if (entry.isIntersecting) {\n      import('./components/lazy-component.js')\n        .then(() =&gt; {\n          // Component is now registered\n          observer.unobserve(entry.target);\n        });\n    }\n  });\n}, { rootMargin: '100px' });\n\n// Observe placeholder elements\ndocument.querySelectorAll('.component-placeholder')\n  .forEach(el =&gt; observer.observe(el));\n</code></pre>\n<p><strong>Interactive Demo:</strong> See lazy-loading in action</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Lazy-Loading Web Components</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;demo-container&quot;&gt;\n  &lt;h1&gt;Lazy-Loading Web Components&lt;/h1&gt;\n  &lt;p&gt;This demo shows how lazy-loading can improve performance by loading components only when needed.&lt;/p&gt;\n  \n  &lt;div class=&quot;performance-comparison&quot;&gt;\n    &lt;div class=&quot;tabs&quot;&gt;\n      &lt;button class=&quot;tab active&quot; data-tab=&quot;eager&quot;&gt;Eager Loading&lt;/button&gt;\n      &lt;button class=&quot;tab&quot; data-tab=&quot;dynamic&quot;&gt;Dynamic Import&lt;/button&gt;\n      &lt;button class=&quot;tab&quot; data-tab=&quot;intersection&quot;&gt;Intersection Observer&lt;/button&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;tab-content&quot; id=&quot;eager-tab&quot;&gt;\n      &lt;h3&gt;Eager Loading (All At Once)&lt;/h3&gt;\n      &lt;p&gt;All components are loaded immediately, which can slow down initial page load.&lt;/p&gt;\n      \n      &lt;div class=&quot;metrics&quot;&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Components Loaded:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;eager-count&quot;&gt;0&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Initial Load Time:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;eager-time&quot;&gt;-&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;button class=&quot;demo-button&quot; id=&quot;load-eager&quot;&gt;Load 10 Eager Components&lt;/button&gt;\n      &lt;div class=&quot;component-container&quot; id=&quot;eager-components&quot;&gt;&lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;tab-content hidden&quot; id=&quot;dynamic-tab&quot;&gt;\n      &lt;h3&gt;Dynamic Import (On Demand)&lt;/h3&gt;\n      &lt;p&gt;Components are loaded on-click, improving initial page load but requiring user action.&lt;/p&gt;\n      \n      &lt;div class=&quot;metrics&quot;&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Components Loaded:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;dynamic-count&quot;&gt;0&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Load Time:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;dynamic-time&quot;&gt;-&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;button class=&quot;demo-button&quot; id=&quot;load-dynamic&quot;&gt;Load Components On-Demand&lt;/button&gt;\n      &lt;div class=&quot;component-container&quot; id=&quot;dynamic-components&quot;&gt;&lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;tab-content hidden&quot; id=&quot;intersection-tab&quot;&gt;\n      &lt;h3&gt;Intersection Observer (Viewport-Based)&lt;/h3&gt;\n      &lt;p&gt;Components load automatically when scrolled into view - the best of both worlds.&lt;/p&gt;\n      \n      &lt;div class=&quot;metrics&quot;&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Components Visible:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;intersection-count&quot;&gt;0&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;metric&quot;&gt;\n          &lt;div class=&quot;metric-title&quot;&gt;Load Time per Component:&lt;/div&gt;\n          &lt;div class=&quot;metric-value&quot; id=&quot;intersection-time&quot;&gt;-&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;p&gt;Scroll down to see components load as they enter the viewport:&lt;/p&gt;\n      &lt;div class=&quot;component-container&quot; id=&quot;intersection-components&quot;&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;1&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;2&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;3&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;4&quot;&gt;Component will load when visible&lt;/div&gt;\n        &lt;div class=&quot;component-placeholder&quot; data-index=&quot;5&quot;&gt;Component will load when visible&lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;code-examples&quot;&gt;\n    &lt;h2&gt;Implementation Examples&lt;/h2&gt;\n    \n    &lt;div class=&quot;code-block&quot;&gt;\n      &lt;h3&gt;Dynamic Import Pattern&lt;/h3&gt;\n      &lt;pre&gt;&lt;code&gt;// Only load when the user takes an action\nconst loadComponent = async () =&gt; {\n  const startTime = performance.now();\n  \n  // Dynamic import of the component module\n  const { MyComponent } = await import(&#39;./my-component.js&#39;);\n  \n  // Register the component only when needed\n  customElements.define(&#39;my-component&#39;, MyComponent);\n  \n  // Create and add the component to the page\n  const component = document.createElement(&#39;my-component&#39;);\n  document.querySelector(&#39;#container&#39;).appendChild(component);\n  \n  const loadTime = performance.now() - startTime;\n  console.log(`Component loaded in ${loadTime}ms`);\n}&lt;/code&gt;&lt;/pre&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;code-block&quot;&gt;\n      &lt;h3&gt;Intersection Observer Pattern&lt;/h3&gt;\n      &lt;pre&gt;&lt;code&gt;// Create observer for viewport detection\nconst observer = new IntersectionObserver((entries) =&gt; {\n  entries.forEach(entry =&gt; {\n    if (entry.isIntersecting) {\n      const placeholder = entry.target;\n      \n      // Load component when placeholder is visible\n      import(&#39;./lazy-component.js&#39;)\n        .then(module =&gt; {\n          // Replace placeholder with actual component\n          const component = document.createElement(&#39;lazy-component&#39;);\n          placeholder.parentNode.replaceChild(component, placeholder);\n          \n          // Stop observing this element\n          observer.unobserve(placeholder);\n        });\n    }\n  });\n}, { rootMargin: &#39;100px&#39; });\n\n// Start observing placeholders\ndocument.querySelectorAll(&#39;.component-placeholder&#39;)\n  .forEach(el =&gt; observer.observe(el));&lt;/code&gt;&lt;/pre&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 900px;\n  margin: 0 auto;\n  padding: 20px;\n  line-height: 1.6;\n  color: #333;\n}\n\nh1, h2, h3 {\n  color: #2c3e50;\n}\n\n.demo-container {\n  margin: 20px 0;\n}\n\n.performance-comparison {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  overflow: hidden;\n  margin: 30px 0;\n}\n\n.tabs {\n  display: flex;\n  background-color: #f0f0f0;\n  border-bottom: 1px solid #ddd;\n}\n\n.tab {\n  padding: 12px 20px;\n  background: none;\n  border: none;\n  cursor: pointer;\n  font-size: 16px;\n  font-weight: 500;\n  color: #666;\n  transition: background-color 0.2s;\n}\n\n.tab:hover {\n  background-color: #e0e0e0;\n}\n\n.tab.active {\n  background-color: white;\n  color: #2c3e50;\n  border-bottom: 3px solid #3498db;\n}\n\n.tab-content {\n  padding: 20px;\n  background-color: white;\n}\n\n.hidden {\n  display: none;\n}\n\n.metrics {\n  display: flex;\n  gap: 20px;\n  margin: 20px 0;\n  padding: 15px;\n  background-color: #f8f9fa;\n  border-radius: 8px;\n}\n\n.metric {\n  flex: 1;\n}\n\n.metric-title {\n  font-weight: bold;\n  margin-bottom: 5px;\n  color: #666;\n}\n\n.metric-value {\n  font-size: 24px;\n  color: #3498db;\n}\n\n.demo-button {\n  background-color: #3498db;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  padding: 10px 20px;\n  font-size: 16px;\n  cursor: pointer;\n  transition: background-color 0.2s;\n}\n\n.demo-button:hover {\n  background-color: #2980b9;\n}\n\n.component-container {\n  margin-top: 20px;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n  gap: 15px;\n}\n\n.component-placeholder {\n  height: 150px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  border: 2px dashed #ccc;\n  border-radius: 8px;\n  background-color: #f5f5f5;\n  color: #777;\n  padding: 10px;\n}\n\n.code-examples {\n  margin-top: 40px;\n}\n\n.code-block {\n  margin: 20px 0;\n}\n\n.code-block pre {\n  background-color: #f5f5f5;\n  padding: 15px;\n  border-radius: 8px;\n  overflow-x: auto;\n  border: 1px solid #ddd;\n}\n\n.code-block code {\n  font-family: &#39;Consolas&#39;, &#39;Monaco&#39;, &#39;Courier New&#39;, monospace;\n  font-size: 14px;\n  line-height: 1.5;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// HTML\n\n\n\n// Define fancy component to be loaded\nclass FancyComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._rendered = false;\n    this._index = this.getAttribute(&#39;data-index&#39;) || &#39;?&#39;;\n    this._loadTime = this.getAttribute(&#39;data-load-time&#39;) || &#39;0&#39;;\n    this.render();\n  }\n\n  connectedCallback() {\n    if (!this._rendered) {\n      this.render();\n    }\n\n    // Simulate real-world component initialization work\n    this.simulateWork();\n  }\n\n  simulateWork() {\n    // CPU-intensive operation to simulate real component work\n    let result = 0;\n    for (let i = 0; i &lt; 100000; i++) {\n      result += Math.sqrt(i) * Math.random();\n    }\n\n    this.shadowRoot.querySelector(&#39;.value&#39;).textContent = result.toFixed(0);\n  }\n\n  render() {\n    this._rendered = true;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          border-radius: 8px;\n          overflow: hidden;\n          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n          background-color: white;\n          transition: transform 0.2s;\n        }\n        \n        :host(:hover) {\n          transform: translateY(-5px);\n        }\n        \n        .header {\n          background-color: #3498db;\n          color: white;\n          padding: 12px;\n          font-weight: bold;\n          text-align: center;\n        }\n        \n        .body {\n          padding: 15px;\n          text-align: center;\n        }\n        \n        .info {\n          display: flex;\n          justify-content: space-between;\n          margin-top: 15px;\n          font-size: 12px;\n          color: #777;\n        }\n        \n        .value {\n          font-size: 24px;\n          color: #2c3e50;\n          margin: 10px 0;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;header&quot;&gt;Component #${this._index}&lt;/div&gt;\n      &lt;div class=&quot;body&quot;&gt;\n        &lt;div&gt;Calculated Value:&lt;/div&gt;\n        &lt;div class=&quot;value&quot;&gt;Calculating...&lt;/div&gt;\n        &lt;div class=&quot;info&quot;&gt;\n          &lt;span&gt;Load time: ${this._loadTime}ms&lt;/span&gt;\n          &lt;span&gt;Memory: ~${(Math.random() * 5 + 1).toFixed(1)}MB&lt;/span&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Initialize tabs\nconst tabs = document.querySelectorAll(&#39;.tab&#39;);\ntabs.forEach(tab =&gt; {\n  tab.addEventListener(&#39;click&#39;, () =&gt; {\n    // Deactivate all tabs\n    tabs.forEach(t =&gt; t.classList.remove(&#39;active&#39;));\n\n    // Hide all content\n    document.querySelectorAll(&#39;.tab-content&#39;).forEach(content =&gt; {\n      content.classList.add(&#39;hidden&#39;);\n    });\n\n    // Activate clicked tab\n    tab.classList.add(&#39;active&#39;);\n\n    // Show corresponding content\n    const tabId = tab.getAttribute(&#39;data-tab&#39;);\n    document.getElementById(`${tabId}-tab`).classList.remove(&#39;hidden&#39;);\n  });\n});\n\n// Register the component for eager loading\ncustomElements.define(&#39;fancy-component&#39;, FancyComponent);\n\n// Eager loading demo\ndocument.getElementById(&#39;load-eager&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;eager-components&#39;);\n  container.innerHTML = &#39;&#39;;\n\n  const startTime = performance.now();\n\n  // Create 10 components at once\n  for (let i = 1; i &lt;= 10; i++) {\n    const component = document.createElement(&#39;fancy-component&#39;);\n    component.setAttribute(&#39;data-index&#39;, i);\n    container.appendChild(component);\n  }\n\n  const totalTime = performance.now() - startTime;\n\n  // Update metrics\n  document.getElementById(&#39;eager-count&#39;).textContent = &#39;10&#39;;\n  document.getElementById(&#39;eager-time&#39;).textContent = `${totalTime.toFixed(1)}ms`;\n});\n\n// Dynamic import demo\ndocument.getElementById(&#39;load-dynamic&#39;).addEventListener(&#39;click&#39;, async () =&gt; {\n  const container = document.getElementById(&#39;dynamic-components&#39;);\n  container.innerHTML = &#39;&#39;;\n\n  const startTime = performance.now();\n\n  // Load components one by one with a small delay to better see the process\n  for (let i = 1; i &lt;= 5; i++) {\n    const componentStartTime = performance.now();\n\n    // Simulate dynamic import (we already have the component)\n    await new Promise(resolve =&gt; setTimeout(resolve, 200));\n\n    const component = document.createElement(&#39;fancy-component&#39;);\n    component.setAttribute(&#39;data-index&#39;, i);\n    component.setAttribute(&#39;data-load-time&#39;, (performance.now() - componentStartTime).toFixed(1));\n    container.appendChild(component);\n\n    // Update count\n    document.getElementById(&#39;dynamic-count&#39;).textContent = i;\n  }\n\n  const totalTime = performance.now() - startTime;\n  document.getElementById(&#39;dynamic-time&#39;).textContent = `${totalTime.toFixed(1)}ms`;\n});\n\n// Intersection Observer demo\nconst observerOptions = {\n  rootMargin: &#39;0px&#39;,\n  threshold: 0.1 };\n\n\nconst componentObserver = new IntersectionObserver(entries =&gt; {\n  entries.forEach(entry =&gt; {\n    if (entry.isIntersecting) {\n      const placeholder = entry.target;\n      const index = placeholder.getAttribute(&#39;data-index&#39;);\n\n      // Measure loading time\n      const startTime = performance.now();\n\n      // Simulate dynamic import\n      setTimeout(() =&gt; {\n        const component = document.createElement(&#39;fancy-component&#39;);\n        component.setAttribute(&#39;data-index&#39;, index);\n\n        const loadTime = performance.now() - startTime;\n        component.setAttribute(&#39;data-load-time&#39;, loadTime.toFixed(1));\n\n        // Replace placeholder with actual component\n        placeholder.parentNode.replaceChild(component, placeholder);\n\n        // Update metrics\n        const countEl = document.getElementById(&#39;intersection-count&#39;);\n        countEl.textContent = parseInt(countEl.textContent || &#39;0&#39;) + 1;\n\n        document.getElementById(&#39;intersection-time&#39;).textContent = `${loadTime.toFixed(1)}ms`;\n      }, 300); // Simulated import delay\n\n      // Stop observing this element\n      componentObserver.unobserve(placeholder);\n    }\n  });\n}, observerOptions);\n\n// Start observing placeholders\ndocument.querySelectorAll(&#39;.component-placeholder&#39;).forEach(placeholder =&gt; {\n  componentObserver.observe(placeholder);\n});\n\n// Initialize counters\ndocument.getElementById(&#39;eager-count&#39;).textContent = &#39;0&#39;;\ndocument.getElementById(&#39;dynamic-count&#39;).textContent = &#39;0&#39;;\ndocument.getElementById(&#39;intersection-count&#39;).textContent = &#39;0&#39;;\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h3 id=\"shadow-dom-performance\">Shadow DOM Performance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#shadow-dom-performance\" aria-label=\"Link to this section\">#</a></h3>\n<p>The Shadow DOM provides encapsulation but comes with performance considerations:</p>\n<ol>\n<li>\n<p><strong>Containment for Performance</strong></p>\n<pre><code class=\"language-css\"> :host {\n   contain: content; /* Limit style recalculation scope */\n   container-type: inline-size; /* For container queries */\n }\n</code></pre>\n</li>\n<li>\n<p><strong>Minimizing Selectors</strong></p>\n<pre><code class=\"language-css\"> /* Avoid overly complex selectors that cross shadow boundaries */\n :host ::slotted(*) &gt; * { /* Expensive */ }\n\n /* Better to use direct, simple selectors */\n .my-item { /* More efficient */ }\n</code></pre>\n</li>\n<li>\n<p><strong>Slot Change Optimization</strong></p>\n<pre><code class=\"language-javascript\"> // Listen only when needed\n constructor() {\n   super();\n   this.attachShadow({ mode: 'open' }).innerHTML = '&lt;slot&gt;&lt;/slot&gt;';\n   this.shadowRoot.querySelector('slot')\n     .addEventListener('slotchange', this._handleSlotChange);\n }\n\n // Debounce frequent updates\n _handleSlotChange = debounce(() =&gt; {\n   this._processSlottedChildren();\n }, 100);\n</code></pre>\n</li>\n</ol>\n<h3 id=\"render-optimization\">Render Optimization<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#render-optimization\" aria-label=\"Link to this section\">#</a></h3>\n<p>Efficient rendering patterns can dramatically improve performance:</p>\n<ol>\n<li>\n<p><strong>Batched DOM Updates</strong></p>\n<pre><code class=\"language-javascript\"> // Poor: Multiple separate DOM operations\n this.shadowRoot.querySelector('.title').textContent = title;\n this.shadowRoot.querySelector('.desc').textContent = description;\n\n // Better: Batch DOM operations with DocumentFragment\n update(data) {\n   const fragment = document.createDocumentFragment();\n   // Build complete update in memory\n   const title = document.createElement('div');\n   title.className = 'title';\n   title.textContent = data.title;\n   fragment.appendChild(title);\n   // etc...\n\n   // Single DOM operation\n   const container = this.shadowRoot.querySelector('.container');\n   container.innerHTML = '';\n   container.appendChild(fragment);\n }\n</code></pre>\n</li>\n<li>\n<p><strong>RequestAnimationFrame for Visual Updates</strong></p>\n<pre><code class=\"language-javascript\"> updateVisuals() {\n   // Schedule visual updates in animation frame\n   requestAnimationFrame(() =&gt; {\n     this.elements.forEach(el =&gt; {\n       el.style.transform = `translate(${this.x}px, ${this.y}px)`;\n     });\n   });\n }\n</code></pre>\n</li>\n</ol>\n<p><strong>Interactive Demo:</strong> Compare optimized vs. unoptimized components</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Web Component Render Optimization</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;demo-container&quot;&gt;\n  &lt;h1&gt;Web Component Render Optimization&lt;/h1&gt;\n  &lt;p&gt;Compare optimized vs. unoptimized rendering approaches in Web Components&lt;/p&gt;\n  \n  &lt;div class=&quot;tabs&quot;&gt;\n    &lt;button class=&quot;tab active&quot; data-tab=&quot;basic&quot;&gt;Basic Comparison&lt;/button&gt;\n    &lt;button class=&quot;tab&quot; data-tab=&quot;techniques&quot;&gt;Optimization Techniques&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;tab-content&quot; id=&quot;basic-tab&quot;&gt;\n    &lt;h2&gt;Performance Comparison&lt;/h2&gt;\n    &lt;p&gt;Watch the difference between optimized and unoptimized component updates when handling frequent state changes.&lt;/p&gt;\n    \n    &lt;div class=&quot;control-panel&quot;&gt;\n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;update-frequency&quot;&gt;Update Frequency:&lt;/label&gt;\n        &lt;select id=&quot;update-frequency&quot;&gt;\n          &lt;option value=&quot;500&quot;&gt;Low (500ms)&lt;/option&gt;\n          &lt;option value=&quot;100&quot;&gt;Medium (100ms)&lt;/option&gt;\n          &lt;option value=&quot;16&quot; selected&gt;High (16ms)&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;item-count&quot;&gt;Item Count:&lt;/label&gt;\n        &lt;select id=&quot;item-count&quot;&gt;\n          &lt;option value=&quot;10&quot;&gt;Few (10)&lt;/option&gt;\n          &lt;option value=&quot;50&quot; selected&gt;Medium (50)&lt;/option&gt;\n          &lt;option value=&quot;100&quot;&gt;Many (100)&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n      \n      &lt;button id=&quot;start-test&quot; class=&quot;demo-button&quot;&gt;Start Comparison&lt;/button&gt;\n      &lt;button id=&quot;stop-test&quot; class=&quot;demo-button&quot; disabled&gt;Stop&lt;/button&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;performance-results&quot;&gt;\n      &lt;div class=&quot;result-box&quot;&gt;\n        &lt;h3&gt;Unoptimized Component&lt;/h3&gt;\n        &lt;div class=&quot;metrics&quot;&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Frame Rate:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;unoptimized-fps&quot;&gt;0 FPS&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Updates:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;unoptimized-updates&quot;&gt;0&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div id=&quot;unoptimized-container&quot; class=&quot;component-container&quot;&gt;\n          &lt;div class=&quot;placeholder&quot;&gt;Click &quot;Start Comparison&quot; to initialize&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;result-box&quot;&gt;\n        &lt;h3&gt;Optimized Component&lt;/h3&gt;\n        &lt;div class=&quot;metrics&quot;&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Frame Rate:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;optimized-fps&quot;&gt;0 FPS&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;metric&quot;&gt;\n            &lt;div class=&quot;metric-title&quot;&gt;Updates:&lt;/div&gt;\n            &lt;div class=&quot;metric-value&quot; id=&quot;optimized-updates&quot;&gt;0&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div id=&quot;optimized-container&quot; class=&quot;component-container&quot;&gt;\n          &lt;div class=&quot;placeholder&quot;&gt;Click &quot;Start Comparison&quot; to initialize&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;tab-content hidden&quot; id=&quot;techniques-tab&quot;&gt;\n    &lt;h2&gt;Optimization Techniques&lt;/h2&gt;\n    \n    &lt;div class=&quot;technique&quot;&gt;\n      &lt;h3&gt;1. Batched DOM Updates&lt;/h3&gt;\n      &lt;p&gt;Instead of updating the DOM multiple times, batch changes into a single update using DocumentFragment.&lt;/p&gt;\n      \n      &lt;div class=&quot;code-comparison&quot;&gt;\n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Unoptimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Unoptimized: Multiple separate DOM operations\nupdateItems(items) {\n  // Clear existing items\n  this.listElement.innerHTML = &#39;&#39;;\n  \n  // Add each item individually (multiple DOM operations)\n  items.forEach(item =&gt; {\n    const li = document.createElement(&#39;li&#39;);\n    li.textContent = item.text;\n    this.listElement.appendChild(li);\n  });\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Optimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Optimized: Single DOM operation with DocumentFragment\nupdateItems(items) {\n  // Create fragment (in-memory container)\n  const fragment = document.createDocumentFragment();\n  \n  // Build complete structure in memory\n  items.forEach(item =&gt; {\n    const li = document.createElement(&#39;li&#39;);\n    li.textContent = item.text;\n    fragment.appendChild(li);\n  });\n  \n  // Single DOM operation\n  this.listElement.innerHTML = &#39;&#39;;\n  this.listElement.appendChild(fragment);\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;technique&quot;&gt;\n      &lt;h3&gt;2. RequestAnimationFrame for Visual Updates&lt;/h3&gt;\n      &lt;p&gt;Schedule visual updates to align with the browser&#39;s render cycle for smoother animations.&lt;/p&gt;\n      \n      &lt;div class=&quot;code-comparison&quot;&gt;\n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Unoptimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Unoptimized: Direct style changes\nupdatePosition(x, y) {\n  // This might cause layout thrashing if called frequently\n  this.element.style.transform = `translate(${x}px, ${y}px)`;\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Optimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Optimized: RAF for smooth visual updates\nupdatePosition(x, y) {\n  // Store the values to update\n  this._pendingX = x;\n  this._pendingY = y;\n  \n  // Only schedule update if none is pending\n  if (!this._updateScheduled) {\n    this._updateScheduled = true;\n    \n    requestAnimationFrame(() =&gt; {\n      // Apply all pending updates at once\n      this.element.style.transform = \n        `translate(${this._pendingX}px, ${this._pendingY}px)`;\n      \n      this._updateScheduled = false;\n    });\n  }\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;technique&quot;&gt;\n      &lt;h3&gt;3. Debouncing Property Changes&lt;/h3&gt;\n      &lt;p&gt;Limit the frequency of updates when properties change rapidly.&lt;/p&gt;\n      \n      &lt;div class=&quot;code-comparison&quot;&gt;\n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Unoptimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Unoptimized: Update on every property change\nset value(newValue) {\n  this._value = newValue;\n  this.render(); // May be called too frequently\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;code-block&quot;&gt;\n          &lt;h4&gt;Optimized Approach&lt;/h4&gt;\n          &lt;pre&gt;&lt;code&gt;// Optimized: Debounced updates\nconstructor() {\n  super();\n  this._value = 0;\n  this._debounceTimeout = null;\n}\n\nset value(newValue) {\n  this._value = newValue;\n  \n  // Clear previous timeout\n  clearTimeout(this._debounceTimeout);\n  \n  // Schedule a new update with a delay\n  this._debounceTimeout = setTimeout(() =&gt; {\n    this.render();\n  }, 16); // Roughly one frame at 60fps\n}&lt;/code&gt;&lt;/pre&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, Oxygen, Ubuntu, Cantarell, &#39;Open Sans&#39;, &#39;Helvetica Neue&#39;, sans-serif;\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: 20px;\n  line-height: 1.6;\n  color: #333;\n  background-color: #f8f9fa;\n}\n\nh1, h2, h3, h4 {\n  color: #2c3e50;\n}\n\n.demo-container {\n  margin: 20px 0;\n}\n\n.tabs {\n  display: flex;\n  gap: 2px;\n  margin-bottom: 20px;\n}\n\n.tab {\n  padding: 10px 20px;\n  background-color: #e9ecef;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.2s;\n}\n\n.tab:hover {\n  background-color: #dee2e6;\n}\n\n.tab.active {\n  background-color: #3498db;\n  color: white;\n}\n\n.tab-content {\n  background-color: white;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.hidden {\n  display: none;\n}\n\n.control-panel {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 15px;\n  align-items: center;\n  background-color: #f0f7ff;\n  padding: 15px;\n  border-radius: 8px;\n  margin-bottom: 20px;\n}\n\n.control {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n}\n\nlabel {\n  font-weight: 500;\n}\n\nselect {\n  padding: 8px;\n  border-radius: 4px;\n  border: 1px solid #ced4da;\n  background-color: white;\n  font-size: 16px;\n}\n\n.demo-button {\n  background-color: #3498db;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  padding: 8px 16px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.2s;\n}\n\n.demo-button:hover {\n  background-color: #2980b9;\n}\n\n.demo-button:disabled {\n  background-color: #95a5a6;\n  cursor: not-allowed;\n}\n\n.performance-results {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 20px;\n}\n\n.result-box {\n  background-color: white;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n\n.metrics {\n  display: flex;\n  gap: 20px;\n  margin: 15px 0;\n  padding: 15px;\n  background-color: #f8f9fa;\n  border-radius: 8px;\n}\n\n.metric {\n  flex: 1;\n}\n\n.metric-title {\n  font-weight: bold;\n  margin-bottom: 5px;\n  color: #666;\n}\n\n.metric-value {\n  font-size: 24px;\n  color: #3498db;\n}\n\n.component-container {\n  border: 1px solid #e9ecef;\n  border-radius: 8px;\n  padding: 15px;\n  height: 300px;\n  overflow: auto;\n  background-color: #f8f9fa;\n}\n\n.placeholder {\n  display: flex;\n  height: 100%;\n  align-items: center;\n  justify-content: center;\n  color: #7f8c8d;\n  font-style: italic;\n}\n\n.technique {\n  margin-bottom: 40px;\n  padding-bottom: 20px;\n  border-bottom: 1px solid #e9ecef;\n}\n\n.code-comparison {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 20px;\n  margin-top: 15px;\n}\n\n.code-block {\n  background-color: #f8f9fa;\n  border-radius: 8px;\n  overflow: hidden;\n  border: 1px solid #e9ecef;\n}\n\n.code-block h4 {\n  margin: 0;\n  padding: 10px 15px;\n  background-color: #e9ecef;\n  color: #2c3e50;\n}\n\n.code-block pre {\n  margin: 0;\n  padding: 15px;\n  overflow-x: auto;\n}\n\n.code-block code {\n  font-family: &#39;Consolas&#39;, &#39;Monaco&#39;, &#39;Courier New&#39;, monospace;\n}\n\n/* Mobile responsiveness */\n@media (max-width: 768px) {\n  .performance-results, .code-comparison {\n    grid-template-columns: 1fr;\n  }\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class UnoptimizedGrid extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.items = [];\n    this.updateCount = 0;\n    this.lastFrameTime = 0;\n    this.frameCount = 0;\n    this.fps = 0;\n\n    // Initialize the component\n    this.render();\n  }\n\n  // Called when the element is added to the document\n  connectedCallback() {\n    // Start FPS counter\n    this.startFpsCounter();\n  }\n\n  // Called when the element is removed from the document\n  disconnectedCallback() {\n    // Clean up\n    this.stopFpsCounter();\n  }\n\n  // Method to update the data items\n  updateData(items) {\n    this.items = items;\n    this.updateCount++;\n\n    // Update the metrics display\n    this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n      detail: {\n        updates: this.updateCount,\n        fps: this.fps } }));\n\n\n\n    // Render immediately - this is unoptimized\n    this.render();\n  }\n\n  // Unoptimized render method - rebuilds everything on each update\n  render() {\n    // Create the basic structure if it doesn&#39;t exist\n    if (!this.shadowRoot.querySelector(&#39;.grid&#39;)) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          :host {\n            display: block;\n            width: 100%;\n          }\n          \n          .grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\n            gap: 10px;\n            padding: 5px;\n          }\n          \n          .grid-item {\n            background-color: #3498db;\n            color: white;\n            padding: 10px;\n            border-radius: 4px;\n            text-align: center;\n            transition: background-color 0.2s;\n          }\n          \n          .grid-item:hover {\n            background-color: #2980b9;\n          }\n        &lt;/style&gt;\n        &lt;div class=&quot;grid&quot;&gt;&lt;/div&gt;\n      `;\n    }\n\n    const grid = this.shadowRoot.querySelector(&#39;.grid&#39;);\n\n    // Unoptimized: Clear and rebuild everything\n    grid.innerHTML = &#39;&#39;;\n\n    // Unoptimized: Add each item individually (causes multiple reflows)\n    this.items.forEach(item =&gt; {\n      const itemEl = document.createElement(&#39;div&#39;);\n      itemEl.className = &#39;grid-item&#39;;\n      itemEl.textContent = item.value;\n      grid.appendChild(itemEl);\n    });\n  }\n\n  // FPS tracking methods\n  startFpsCounter() {\n    this.frameCount = 0;\n    this.lastFrameTime = performance.now();\n    this.fpsInterval = setInterval(() =&gt; {\n      const now = performance.now();\n      const delta = now - this.lastFrameTime;\n\n      this.fps = Math.round(this.frameCount * 1000 / delta);\n\n      // Reset for next calculation\n      this.frameCount = 0;\n      this.lastFrameTime = now;\n\n      // Dispatch metrics update event\n      this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n        detail: {\n          updates: this.updateCount,\n          fps: this.fps } }));\n\n\n    }, 1000);\n  }\n\n  stopFpsCounter() {\n    clearInterval(this.fpsInterval);\n  }\n\n  // Called for each frame render\n  countFrame() {\n    this.frameCount++;\n  }}\n\n\n// Optimized Data Grid Component\nclass OptimizedGrid extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.items = [];\n    this.updateCount = 0;\n    this.lastFrameTime = 0;\n    this.frameCount = 0;\n    this.fps = 0;\n\n    // Flag to track if a render has been scheduled\n    this._updatePending = false;\n\n    // Initialize the component\n    this.render();\n  }\n\n  // Called when the element is added to the document\n  connectedCallback() {\n    // Start FPS counter\n    this.startFpsCounter();\n  }\n\n  // Called when the element is removed from the document\n  disconnectedCallback() {\n    // Clean up\n    this.stopFpsCounter();\n  }\n\n  // Method to update the data items\n  updateData(items) {\n    this.items = items;\n    this.updateCount++;\n\n    // Update the metrics display\n    this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n      detail: {\n        updates: this.updateCount,\n        fps: this.fps } }));\n\n\n\n    // Optimized: Schedule a render using requestAnimationFrame\n    this.scheduleRender();\n  }\n\n  // Optimization 1: Schedule renders for better performance\n  scheduleRender() {\n    // Only schedule a new render if one isn&#39;t already pending\n    if (!this._updatePending) {\n      this._updatePending = true;\n\n      requestAnimationFrame(() =&gt; {\n        this.render();\n        this._updatePending = false;\n      });\n    }\n  }\n\n  // Optimized render method - uses DocumentFragment for batch updates\n  render() {\n    // Create the basic structure if it doesn&#39;t exist\n    if (!this.shadowRoot.querySelector(&#39;.grid&#39;)) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          :host {\n            display: block;\n            width: 100%;\n          }\n          \n          .grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\n            gap: 10px;\n            padding: 5px;\n          }\n          \n          .grid-item {\n            background-color: #3498db;\n            color: white;\n            padding: 10px;\n            border-radius: 4px;\n            text-align: center;\n            transition: background-color 0.2s;\n          }\n          \n          .grid-item:hover {\n            background-color: #2980b9;\n          }\n        &lt;/style&gt;\n        &lt;div class=&quot;grid&quot;&gt;&lt;/div&gt;\n      `;\n    }\n\n    const grid = this.shadowRoot.querySelector(&#39;.grid&#39;);\n\n    // Optimization 2: Use DocumentFragment for batched DOM updates\n    const fragment = document.createDocumentFragment();\n\n    // Build all elements in memory first\n    this.items.forEach(item =&gt; {\n      const itemEl = document.createElement(&#39;div&#39;);\n      itemEl.className = &#39;grid-item&#39;;\n      itemEl.textContent = item.value;\n      fragment.appendChild(itemEl);\n    });\n\n    // Optimization 3: Single DOM operation\n    grid.innerHTML = &#39;&#39;;\n    grid.appendChild(fragment);\n  }\n\n  // FPS tracking methods\n  startFpsCounter() {\n    this.frameCount = 0;\n    this.lastFrameTime = performance.now();\n    this.fpsInterval = setInterval(() =&gt; {\n      const now = performance.now();\n      const delta = now - this.lastFrameTime;\n\n      this.fps = Math.round(this.frameCount * 1000 / delta);\n\n      // Reset for next calculation\n      this.frameCount = 0;\n      this.lastFrameTime = now;\n\n      // Dispatch metrics update event\n      this.dispatchEvent(new CustomEvent(&#39;metrics-update&#39;, {\n        detail: {\n          updates: this.updateCount,\n          fps: this.fps } }));\n\n\n    }, 1000);\n  }\n\n  stopFpsCounter() {\n    clearInterval(this.fpsInterval);\n  }\n\n  // Called for each frame render\n  countFrame() {\n    this.frameCount++;\n  }}\n\n\n// Register our Web Components\ncustomElements.define(&#39;unoptimized-grid&#39;, UnoptimizedGrid);\ncustomElements.define(&#39;optimized-grid&#39;, OptimizedGrid);\n\n// Initialize demo\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  // Setup tab navigation\n  const tabs = document.querySelectorAll(&#39;.tab&#39;);\n  tabs.forEach(tab =&gt; {\n    tab.addEventListener(&#39;click&#39;, () =&gt; {\n      // Deactivate all tabs\n      tabs.forEach(t =&gt; t.classList.remove(&#39;active&#39;));\n\n      // Hide all content\n      document.querySelectorAll(&#39;.tab-content&#39;).forEach(content =&gt; {\n        content.classList.add(&#39;hidden&#39;);\n      });\n\n      // Activate clicked tab\n      tab.classList.add(&#39;active&#39;);\n\n      // Show corresponding content\n      const tabId = tab.getAttribute(&#39;data-tab&#39;);\n      document.getElementById(`${tabId}-tab`).classList.remove(&#39;hidden&#39;);\n    });\n  });\n\n  let unoptimizedGrid;\n  let optimizedGrid;\n  let updateInterval;\n  let animationFrameId;\n\n  // Performance test controls\n  const startButton = document.getElementById(&#39;start-test&#39;);\n  const stopButton = document.getElementById(&#39;stop-test&#39;);\n  const frequencySelect = document.getElementById(&#39;update-frequency&#39;);\n  const itemCountSelect = document.getElementById(&#39;item-count&#39;);\n\n  // Function to generate random test data\n  function generateTestData(count) {\n    const data = [];\n    for (let i = 0; i &lt; count; i++) {\n      data.push({\n        id: i,\n        value: Math.floor(Math.random() * 100) });\n\n    }\n    return data;\n  }\n\n  // Update metrics displays\n  function setupMetricsListeners() {\n    unoptimizedGrid.addEventListener(&#39;metrics-update&#39;, e =&gt; {\n      document.getElementById(&#39;unoptimized-fps&#39;).textContent = `${e.detail.fps} FPS`;\n      document.getElementById(&#39;unoptimized-updates&#39;).textContent = e.detail.updates;\n    });\n\n    optimizedGrid.addEventListener(&#39;metrics-update&#39;, e =&gt; {\n      document.getElementById(&#39;optimized-fps&#39;).textContent = `${e.detail.fps} FPS`;\n      document.getElementById(&#39;optimized-updates&#39;).textContent = e.detail.updates;\n    });\n  }\n\n  // Animation frame counter\n  function countFrames() {\n    unoptimizedGrid?.countFrame();\n    optimizedGrid?.countFrame();\n    animationFrameId = requestAnimationFrame(countFrames);\n  }\n\n  // Start test button\n  startButton.addEventListener(&#39;click&#39;, () =&gt; {\n    // Create components if they don&#39;t exist\n    const unoptimizedContainer = document.getElementById(&#39;unoptimized-container&#39;);\n    const optimizedContainer = document.getElementById(&#39;optimized-container&#39;);\n\n    // Clear containers\n    unoptimizedContainer.innerHTML = &#39;&#39;;\n    optimizedContainer.innerHTML = &#39;&#39;;\n\n    // Create components\n    unoptimizedGrid = document.createElement(&#39;unoptimized-grid&#39;);\n    optimizedGrid = document.createElement(&#39;optimized-grid&#39;);\n\n    // Add to DOM\n    unoptimizedContainer.appendChild(unoptimizedGrid);\n    optimizedContainer.appendChild(optimizedGrid);\n\n    // Setup metrics listeners\n    setupMetricsListeners();\n\n    // Start frame counter\n    animationFrameId = requestAnimationFrame(countFrames);\n\n    // Get update frequency and item count\n    const updateFrequency = parseInt(frequencySelect.value);\n    const itemCount = parseInt(itemCountSelect.value);\n\n    // Start the update interval\n    updateInterval = setInterval(() =&gt; {\n      // Generate new random data\n      const data = generateTestData(itemCount);\n\n      // Update both components\n      unoptimizedGrid.updateData(data);\n      optimizedGrid.updateData(data);\n    }, updateFrequency);\n\n    // Update button states\n    startButton.disabled = true;\n    stopButton.disabled = false;\n    frequencySelect.disabled = true;\n    itemCountSelect.disabled = true;\n  });\n\n  // Stop test button\n  stopButton.addEventListener(&#39;click&#39;, () =&gt; {\n    // Clear the update interval\n    clearInterval(updateInterval);\n    cancelAnimationFrame(animationFrameId);\n\n    // Update button states\n    startButton.disabled = false;\n    stopButton.disabled = true;\n    frequencySelect.disabled = false;\n    itemCountSelect.disabled = false;\n  });\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the original article</a>.</p></figure>\n<h2 id=\"advanced-patterns-and-best-practices\">Advanced Patterns and Best Practices<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#advanced-patterns-and-best-practices\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"state-management-across-boundaries\">State Management Across Boundaries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#state-management-across-boundaries\" aria-label=\"Link to this section\">#</a></h3>\n<p>Managing state between Web Components and frameworks requires thoughtful patterns:</p>\n<h4 id=\"1-the-context-pattern\">1. The Context Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-the-context-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<p>Create a shared context accessible to all components:</p>\n<pre><code class=\"language-javascript\">// context.js - Framework agnostic state management\nexport class ComponentContext {\n  constructor() {\n    this._data = {};\n    this._listeners = new Map();\n  }\n\n  get(key) {\n    return this._data[key];\n  }\n\n  set(key, value) {\n    this._data[key] = value;\n    if (this._listeners.has(key)) {\n      this._listeners.get(key).forEach(callback =&gt; callback(value));\n    }\n  }\n\n  subscribe(key, callback) {\n    if (!this._listeners.has(key)) {\n      this._listeners.set(key, new Set());\n    }\n    this._listeners.get(key).add(callback);\n\n    return () =&gt; {\n      this._listeners.get(key).delete(callback);\n    };\n  }\n}\n\n// Create shared instance\nexport const context = new ComponentContext();\n</code></pre>\n<p>Usage in components:</p>\n<pre><code class=\"language-javascript\">import { context } from './context.js';\n\nclass MyComponent extends HTMLElement {\n  connectedCallback() {\n    // Subscribe to state changes\n    this._unsubscribe = context.subscribe('userData', \n      (data) =&gt; this._updateFromState(data));\n\n    // Initial state\n    this._updateFromState(context.get('userData'));\n  }\n\n  disconnectedCallback() {\n    // Clean up subscription\n    this._unsubscribe();\n  }\n\n  _updateState(data) {\n    // Update shared state\n    context.set('userData', data);\n  }\n}\n</code></pre>\n<h4 id=\"2-the-message-bus-pattern\">2. The Message Bus Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#2-the-message-bus-pattern\" aria-label=\"Link to this section\">#</a></h4>\n<p>For loosely coupled components, a pub/sub event bus provides flexible communication:</p>\n<pre><code class=\"language-javascript\">// event-bus.js\nexport class EventBus {\n  constructor() {\n    this.listeners = {};\n  }\n\n  on(event, callback) {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event].push(callback);\n\n    return () =&gt; this.off(event, callback);\n  }\n\n  off(event, callback) {\n    if (this.listeners[event]) {\n      this.listeners[event] = this.listeners[event]\n        .filter(cb =&gt; cb !== callback);\n    }\n  }\n\n  emit(event, data) {\n    if (this.listeners[event]) {\n      this.listeners[event].forEach(callback =&gt; {\n        callback(data);\n      });\n    }\n  }\n}\n\n// Shared instance\nexport const eventBus = new EventBus();\n</code></pre>\n<h3 id=\"server-side-rendering-with-web-components\">Server-Side Rendering with Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#server-side-rendering-with-web-components\" aria-label=\"Link to this section\">#</a></h3>\n<p>Server-side rendering improves initial load performance. With Declarative Shadow DOM (<code>&lt;template shadowrootmode=\"open\"&gt;</code>), the server can send a component's shadow tree as plain HTML:</p>\n<pre><code class=\"language-html\">&lt;!-- Server-rendered output --&gt;\n&lt;my-component&gt;\n  &lt;template shadowrootmode=\"open\"&gt;\n    &lt;style&gt;\n      /* Shadow DOM styles */\n      .card { \n        padding: 16px;\n        border: 1px solid #ddd;\n      }\n    &lt;/style&gt;\n    &lt;div class=\"card\"&gt;\n      &lt;slot&gt;&lt;/slot&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n  &lt;p&gt;This content is slotted from light DOM&lt;/p&gt;\n&lt;/my-component&gt;\n\n&lt;!-- Polyfill for browsers without Declarative Shadow DOM --&gt;\n&lt;script&gt;\n  if (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {\n    document.querySelectorAll('template[shadowrootmode]').forEach(template =&gt; {\n      const mode = template.getAttribute('shadowrootmode');\n      const shadowRoot = template.parentNode.attachShadow({ mode });\n      shadowRoot.appendChild(template.content);\n      template.remove();\n    });\n  }\n&lt;/script&gt;\n</code></pre>\n<h3 id=\"testing-strategies\">Testing Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#testing-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Testing Web Components requires specialized approaches:</p>\n<h4 id=\"1-component-unit-testing\">1. Component Unit Testing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#1-component-unit-testing\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-javascript\">// Using web-test-runner and @open-wc/testing\nimport { html, fixture, expect } from '@open-wc/testing';\nimport '../src/my-component.js';\n\ndescribe('MyComponent', () =&gt; {\n  it('renders with default values', async () =&gt; {\n    const el = await fixture(html`&lt;my-component&gt;&lt;/my-component&gt;`);\n\n    expect(el.shadowRoot.querySelector('.title').textContent)\n      .to.equal('Default Title');\n  });\n\n  it('updates when properties change', async () =&gt; {\n    const el = await fixture(html`&lt;my-component&gt;&lt;/my-component&gt;`);\n\n    el.title = 'New Title';\n    await el.updateComplete; // For LitElement components\n\n    expect(el.shadowRoot.querySelector('.title').textContent)\n      .to.equal('New Title');\n  });\n\n  it('fires custom events', async () =&gt; {\n    const el = await fixture(html`&lt;my-component&gt;&lt;/my-component&gt;`);\n\n    // Setup event listener\n    let eventFired = false;\n    el.addEventListener('custom-event', () =&gt; eventFired = true);\n\n    // Trigger event\n    el.shadowRoot.querySelector('button').click();\n\n    expect(eventFired).to.be.true;\n  });\n});\n</code></pre>\n<h4 id=\"2-integration-testing-with-frameworks\">2. Integration Testing with Frameworks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#2-integration-testing-with-frameworks\" aria-label=\"Link to this section\">#</a></h4>\n<pre><code class=\"language-jsx\">// React Testing Library example\nimport { render, fireEvent, screen } from '@testing-library/react';\nimport MyReactComponent from '../src/MyReactComponent';\n\n// Register Web Component if not done globally\nimport '../src/web-components/my-element.js';\n\ntest('React component interacts with Web Component', async () =&gt; {\n  render(&lt;MyReactComponent initialData=\"test\" /&gt;);\n\n  // Interact with the wrapped Web Component\n  fireEvent.click(screen.getByRole('button'));\n\n  // Check the result\n  expect(screen.getByText('Success')).toBeInTheDocument();\n});\n</code></pre>\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>You can now bridge a framework and a web component:</p>\n<ul>\n<li>Pass objects and arrays as <strong>properties</strong> (through a ref in React), not as attributes.</li>\n<li>Listen for custom events with <strong><code>addEventListener</code></strong>, and remove the listener in the effect's cleanup.</li>\n<li>Wrap that once in a small component so the rest of your app never thinks about it.</li>\n</ul>\n<p>A framework can now hand a component an object and hear its events, which was the whole problem.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Advanced Patterns & Integration with Frameworks\nCanonical: https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\nAdvanced Patterns for Web Components in Modern Frameworks\nPass a JavaScript object to a web component from React, and the component receives the text [object Object]. That is the whole problem this post solves: frameworks and web components pass data in different ways, and a few small habits bridge them.\nWhen a framework such as React meets a custom tag like <user-badge>, data and events can get lost on the way across. This post shows where, and how to stop it.\nPrerequisites in two lines: a custom element can take small settings as HTML attributes (always strings) and richer data as JavaScript properties (el.data = {...}). It talks back with custom events that you listen for with addEventListener.\nReact is covered in full and Vue is the second example. Angular and Svelte follow the same two ideas (property for data, addEventListener for events), so their sections are optional reading.\nWhy Framework Integration Matters\nWeb Components were designed to be universal, but the reality of modern web development means they must coexist with frameworks that have their own component models and lifecycles. Successful integration requires understanding both worlds and the boundaries between them.\nHere is the failure, so you can recognize it. In React 18 (checked with 18.3.1), this element receives a string, not your object:\n<user-badge user={{ name: 'Ada' }} />\n// the DOM now has user=\"[object Object]\", and the component's `user` property is untouched\nReact 18 writes unknown props on custom elements as attributes, and an attribute can only hold text. A function prop such as onChartUpdate={fn} is also not turned into an event listener for a custom event. React 19 changed how props reach custom elements and is not covered here, so check its release notes if you use it.\nWhat makes integration challenging?\nDifferent Data Flow Models: React's unidirectional data flow differs fundamentally from the property-based approach of Web Components.\nEvent Handling Discrepancies: Each framework has its own event system that must be reconciled with the DOM event model.\nLifecycle Management: Coordinating component lifecycle events between frameworks and Web Components requires careful orchestration.\nLet's explore how to overcome these challenges with practical, reusable patterns.\nUsing Web Components with Modern Frameworks\nReact Integration Strategies\nReact's declarative approach to UI challenges direct integration with the imperative DOM APIs of Web Components. Here are advanced patterns to bridge this gap:\n1. The Ref Pattern: Direct DOM Access\nfunction MyReactComponent() {\nconst wcRef = useRef(null);\nuseEffect(() => {\nif (wcRef.current) {\n// Direct property assignment for complex data\nwcRef.current.complexData = { key: \"value\" };\n// Event handling with proper cleanup\nconst el = wcRef.current;\nconst handleEvent = (e) => console.log(e.detail);\nel.addEventListener('custom-event', handleEvent);\nreturn () => {\nel.removeEventListener('custom-event', handleEvent);\n};\n}\n}, []);\nreturn <my-web-component ref={wcRef} string-prop=\"This works\" />;\n}\nKey Insights:\nReact passes string attributes directly, but complex data requires refs\nEvent handling needs manual listeners with proper cleanup\nRef access enables imperative methods on Web Components\nQuestion: A page script wants a component to receive an object. Which call gives the component the object itself?\nel.setAttribute('data', obj)\nel.data = obj\nel.getAttribute('data')\nAnswer: 2. el.data = obj\nExplanation: setAttribute converts its value to a string (\"[object Object]\" for a plain object). Assigning a property keeps the object as it is.\nCoding challenge: Hand React's data to the element\n<user-badge> has a user property, but the React wrapper never sets it, so the preview says \"Hello, nobody yet\".\nExpected result: after you finish the TODO the preview says \"Hello, Ada\".\nHTML\n<div id=\"root\"></div>\nJavaScript\nclass UserBadge extends HTMLElement {\nconstructor() { super(); this.attachShadow({ mode: 'open' }); }\nset user(value) { this._user = value; this.render(); }\nget user() { return this._user; }\nconnectedCallback() { this.render(); }\nrender() {\nconst name = this._user ? this._user.name : 'nobody yet';\nthis.shadowRoot.innerHTML = '<p>Hello, ' + name + '</p>';\n}\n}\ncustomElements.define('user-badge', UserBadge);\nconst { useRef, useEffect, createElement: h } = React;\nfunction Badge({ user }) {\nconst ref = useRef(null);\n// TODO: in a useEffect, set ref.current.user = user (run again when user changes)\nreturn h('user-badge', { ref });\n}\nReactDOM.createRoot(document.getElementById('root')).render(\nh(Badge, { user: { name: 'Ada' } })\n);\nSet a property through a ref on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nTip:\nuseEffect(() => { ref.current.user = user; }, [user]); goes where the TODO is.\n2. The Wrapper Pattern: Creating React-Friendly Components\nReact developers expect components with React-like patterns. Create wrapper components that abstract Web Component peculiarities:\n// Wrapper that handles property mapping and events\nfunction EnhancedWebComponent({ data, onCustomEvent, children }) {\nconst ref = useRef(null);\nuseEffect(() => {\nconst element = ref.current;\nif (element) {\n// Set complex data\nelement.data = data;\n// Handle events\nconst eventHandler = (e) => onCustomEvent(e.detail);\nelement.addEventListener('custom-event', eventHandler);\nreturn () => element.removeEventListener('custom-event', eventHandler);\n}\n}, [data, onCustomEvent]);\nreturn (\n<my-web-component ref={ref}>\n{children}\n</my-web-component>\n);\n}\n// Usage feels like a normal React component\nfunction App() {\nreturn (\n<EnhancedWebComponent\ndata={{complex: \"data\"}}\nonCustomEvent={handleEvent}\n>\n<p>Child content</p>\n</EnhancedWebComponent>\n);\n}\nInteractive Demo: Try the React integration patterns yourself\nHTML\n<div id=\"root\"></div>\nCSS\n.demo-container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.examples {\ndisplay: flex;\nflex-direction: column;\ngap: 20px;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nbackground-color: #fff;\n}\nh2 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.button-group {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\nmargin-top: 8px;\n}\n.react-button {\nbackground-color: #61dafb;\ncolor: #000;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\n}\n.events {\nmargin-top: 16px;\npadding: 8px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\nconst { useState, useRef, useEffect } = React;\n// First, define the Web Component\n// First, define the Web Component\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn ['count'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'count') {\nthis._count = parseInt(newValue || '0', 10);\nthis.render();\n}\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.render();\nthis.dispatchEvent(new CustomEvent('counter-changed', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\nincrement() {\nthis._count++;\nthis.render();\nthis.dispatchEvent(new CustomEvent('counter-changed', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #3498db;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter {\nfont-size: 24px;\nmargin-bottom: 12px;\n}\nbutton {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #2980b9;\n}\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment</button>\n`;\nthis.shadowRoot.querySelector('#increment').addEventListener('click', () => this.increment());\n}\ndisconnectedCallback() {\nconst btn = this.shadowRoot.querySelector('#increment');\nif (btn) {\nbtn.removeEventListener('click', () => this.increment());\n}\n}}\n// Register the web component\nif (!customElements.get('simple-counter')) {\ncustomElements.define('simple-counter', SimpleCounter);\n}\n// React Components using React.createElement for CodePen compatibility\nfunction DirectInteractionExample() {\nconst [events, setEvents] = React.useState([]);\nconst counterRef = React.useRef(null);\nReact.useEffect(() => {\nif (counterRef.current) {\n// Set initial count\ncounterRef.current.count = 5;\n// Set up event listener\nconst handleCounterChanged = e => {\nsetEvents(prev => [...prev, `Counter changed: ${e.detail.count}`]);\n};\ncounterRef.current.addEventListener('counter-changed', handleCounterChanged);\n// Clean up\nreturn () => {\nif (counterRef.current) {\ncounterRef.current.removeEventListener('counter-changed', handleCounterChanged);\n}\n};\n}\n}, []);\n// Match Angular's button actions\nconst handleIncrement = () => {\nif (counterRef.current) {\ncounterRef.current.increment();\n}\n};\nconst handleDecrement = () => {\nif (counterRef.current && counterRef.current.count > 0) {\ncounterRef.current.count = counterRef.current.count - 1;\nsetEvents(prev => [...prev, `Counter decremented to: ${counterRef.current.count}`]);\n}\n};\nconst handleReset = () => {\nif (counterRef.current) {\ncounterRef.current.count = 0;\nsetEvents(prev => [...prev, 'Counter reset to 0']);\n}\n};\nconst handleDoubleValue = () => {\nif (counterRef.current) {\nconst currentValue = counterRef.current.count;\ncounterRef.current.count = currentValue * 2;\nsetEvents(prev => [...prev, `Doubled value to: ${counterRef.current.count}`]);\n}\n};\nreturn React.createElement('div', { className: 'example' },\nReact.createElement('h3', null, 'Using React with Web Components'),\nReact.createElement('p', null, 'Direct component manipulation using refs'),\nReact.createElement('simple-counter', { ref: counterRef }),\nReact.createElement('div', { className: 'button-group' },\nReact.createElement('button', {\nonClick: handleIncrement,\nclassName: 'react-button' },\n'Increment'),\nReact.createElement('button', {\nonClick: handleDecrement,\nclassName: 'react-button' },\n'Decrement'),\nReact.createElement('button', {\nonClick: handleReset,\nclassName: 'react-button' },\n'Reset')),\nReact.createElement('div', { className: 'button-group' },\nReact.createElement('button', {\nonClick: handleDoubleValue,\nclassName: 'react-button' },\n'Double Value')),\nReact.createElement('div', { className: 'events' },\nReact.createElement('strong', null, 'Events:'),\nevents.map((event, i) => React.createElement('div', { key: i }, event))));\n}\n// Main Demo Component\nfunction ReactWebComponentDemo() {\nreturn React.createElement('div', { className: 'demo-container' },\nReact.createElement('h2', null, 'React + Web Components Integration'),\nReact.createElement('p', null,\n'This example demonstrates how React can interact with Web Components through refs and direct DOM manipulation.'),\nReact.createElement(DirectInteractionExample));\n}\n// Render the demo\nReactDOM.render(React.createElement(ReactWebComponentDemo), document.getElementById('root'));\nReact + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nAngular Integration Strategies\nAngular has built-in support for Web Components through its @angular/elements package, but deeper integration still requires specific techniques:\n1. Element Binding with Custom Directives\n@Directive({\nselector: 'my-web-component'\n})\nexport class WebComponentDirective implements OnInit, OnChanges, OnDestroy {\n@Input() complexData: any;\n@Output() customEvent = new EventEmitter<any>();\nconstructor(private el: ElementRef) {}\nngOnInit() {\nthis.el.nativeElement.addEventListener('custom-event',\nthis.handleCustomEvent.bind(this));\n}\nngOnChanges(changes: SimpleChanges) {\nif (changes.complexData) {\nthis.el.nativeElement.complexData = this.complexData;\n}\n}\nngOnDestroy() {\nthis.el.nativeElement.removeEventListener('custom-event',\nthis.handleCustomEvent.bind(this));\n}\nprivate handleCustomEvent(event: CustomEvent) {\nthis.customEvent.emit(event.detail);\n}\n}\nKey Insights:\nAngular directives provide clean property and event binding\nOnChanges lifecycle hook updates properties efficiently\nElementRef gives direct access to the native element\nInteractive Demo: Explore Angular integrations\nHTML\n<div ng-app=\"webComponentDemo\" ng-controller=\"MainController\">\n<div class=\"demo-container\">\n<h2>Angular + Web Components Integration</h2>\n<p>\nThis example demonstrates how Angular can work with Web Components through directive bindings\nand direct DOM access.\n</p>\n<div class=\"examples\">\n<div class=\"example\">\n<h3>Using Angular Directives</h3>\n<p>Binding component state to Angular's data model</p>\n<simple-counter count=\"counterValue\" on-counter-changed=\"handleCounterEvent($event)\">\n</simple-counter>\n<button class=\"angular-button\" ng-click=\"incrementCounter()\">\nAngular: Increment Counter\n</button>\n<div class=\"events\">\n<strong>Current count in Angular: {{ counterValue }}</strong>\n<div ng-repeat=\"event in events track by $index\">\n{{ event }}\n</div>\n</div>\n</div>\n<div class=\"example\">\n<h3>Direct DOM Access</h3>\n<p>Using element references to directly call component methods</p>\n<simple-counter id=\"direct-counter\"></simple-counter>\n<button class=\"angular-button\" ng-click=\"callDirectMethod()\">\nCall Component Method Directly\n</button>\n</div>\n</div>\n</div>\n</div>\nCSS\n.demo-container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nOxygen, Ubuntu, Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.examples {\ndisplay: flex;\nflex-direction: column;\ngap: 20px;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nbackground-color: #fff;\n}\nh2 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.angular-button {\nbackground-color: #dd0031;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\nmargin-top: 8px;\ncursor: pointer;\nfont-size: 16px;\n}\n.events {\nmargin-top: 16px;\npadding: 8px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\n// Define our Web Component\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn [\"count\"];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"count\") {\nthis._count = parseInt(newValue || \"0\", 10);\nthis.render();\n}\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.render();\n// When set programmatically, update the attribute to keep in sync\nthis.setAttribute(\"count\", value);\n}\nincrement() {\nthis._count++;\nthis.render();\nthis.dispatchEvent(\nnew CustomEvent(\"counter-changed\", {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n// Update the attribute to keep in sync with the property\nthis.setAttribute(\"count\", this._count);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #dd0031;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter {\nfont-size: 24px;\nmargin-bottom: 12px;\n}\nbutton {\nbackground-color: #dd0031;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #a50023;\n}\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment from Inside</button>\n`;\nthis.shadowRoot.\nquerySelector(\"#increment\").\naddEventListener(\"click\", () => this.increment());\n}\ndisconnectedCallback() {\nconst button = this.shadowRoot.querySelector(\"#increment\");\nif (button) {\nbutton.removeEventListener(\"click\", () => this.increment());\n}\n}}\n// Register our Web Component\nif (!customElements.get(\"simple-counter\")) {\ncustomElements.define(\"simple-counter\", SimpleCounter);\n}\n// Angular app\nangular.\nmodule(\"webComponentDemo\", []).\ncontroller(\"MainController\", [\n\"$scope\",\n\"$timeout\",\nfunction ($scope, $timeout) {\n// Initialize data\n$scope.counterValue = 5;\n$scope.events = [];\n// Event handler for counter changes\n$scope.handleCounterEvent = function (event) {\n// Use $timeout to ensure Angular knows about the update\n$timeout(function () {\n$scope.counterValue = event.detail.count;\n$scope.events.push(\"Counter changed: \" + event.detail.count);\n});\n};\n// Method to increment the counter from Angular\n$scope.incrementCounter = function () {\n$scope.counterValue++;\n// Angular will update the attribute automatically\n};\n// Method to call the component directly\n$scope.callDirectMethod = function () {\nvar counter = document.getElementById(\"direct-counter\");\nif (counter) {\ncounter.increment();\n// Add to the events list\n$timeout(function () {\n$scope.events.push(\"Called increment() method directly\");\n});\n}\n};\n// Setup event handling for the direct counter\n$timeout(function () {\nvar counter = document.getElementById(\"direct-counter\");\nif (counter) {\n// Set initial value\ncounter.count = 10;\n// Listen for events\ncounter.addEventListener(\"counter-changed\", function (event) {\n$timeout(function () {\n$scope.events.push(\"Direct counter changed: \" + event.detail.count);\n});\n});\n}\n});\n// We need to create a directive to bind our Web Component properly\n// This is done to demonstrate the concept, but in CodePen it's simpler to\n// do the binding manually as we've done above\n}])\n// Add a simple directive to handle attribute binding\n.directive(\"simpleCounter\", function () {\nreturn {\nrestrict: \"E\",\nscope: {\ncount: \"=?\", // Two-way binding (optional: the direct counter has no count attribute)\nonCounterChanged: \"&\" // Function binding\n},\nlink: function (scope, element, attrs) {\n// Watch for changes to the 'count' attribute\nscope.$watch(\"count\", function (newValue) {\nif (newValue !== undefined) {\nelement[0].count = newValue;\n}\n});\n// Listen for counter-changed events\nelement[0].addEventListener(\"counter-changed\", function (event) {\nscope.$evalAsync(function () {\nscope.count = event.detail.count;\nscope.onCounterChanged({ $event: event });\n});\n});\n} };\n});\nAngular + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nVue Integration Strategies\nVue offers excellent built-in support for Web Components with its attribute and event binding syntax:\n<template>\n<my-web-component\n:complex-prop=\"myData\"\n@custom-event=\"handleEvent\"\n/>\n</template>\n<script>\nexport default {\ndata() {\nreturn {\nmyData: { key: 'value' }\n}\n},\nmethods: {\nhandleEvent(event) {\nconsole.log(event.detail);\n}\n},\nmounted() {\n// For cases where direct property access is needed\nthis.$refs.myComponent.directMethod();\n}\n}\n</script>\nKey Insights:\nVue's :prop syntax works with kebab-case attributes\nNative event handling with @event-name\nVue's reactivity system naturally updates Web Component properties\nInteractive Demo: Explore Vue integrations\nHTML\n<div id=\"app\">\n<h2>Vue + Web Components Integration</h2>\n<p>\nVue offers excellent support for Web Components with its built-in directive syntax.\nThis demo shows how properties and events work.\n</p>\n<div class=\"demo-container\">\n<div class=\"example\">\n<h3>Basic Integration</h3>\n<p>Vue's declarative syntax makes Web Component integration straightforward</p>\n<simple-counter :count=\"counterData\" @counter-changed=\"handleCounterEvent\"></simple-counter>\n<button class=\"vue-button\" @click=\"incrementCounter\">\nIncrement from Vue\n</button>\n<div class=\"events\">\n<strong>Current count in Vue data: {{ counterData }}</strong>\n<div v-for=\"(event, index) in events\" :key=\"index\">\n{{ event }}\n</div>\n</div>\n</div>\n<div class=\"example\">\n<h3>Using $refs for Direct Access</h3>\n<p>When needed, Vue can access Web Components directly with $refs</p>\n<simple-counter ref=\"directCounter\"></simple-counter>\n<button class=\"vue-button\" @click=\"callComponentMethod\">\nCall component method directly\n</button>\n</div>\n</div>\n</div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nOxygen, Ubuntu, Cantarell, \"Open Sans\", \"Helvetica Neue\", sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.demo-container {\ndisplay: flex;\nflex-direction: column;\ngap: 20px;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nbackground-color: #fff;\n}\nh2 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.vue-button {\nbackground-color: #42b883;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\nmargin-top: 8px;\ncursor: pointer;\nfont-size: 16px;\n}\n.events {\nmargin-top: 16px;\npadding: 8px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\n// Define Web Component\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn [\"count\"];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"count\") {\nthis._count = parseInt(newValue || \"0\", 10);\nthis.render();\n}\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.render();\n}\nincrement() {\nthis._count++;\nthis.render();\nthis.dispatchEvent(\nnew CustomEvent(\"counter-changed\", {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #42b883;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter {\nfont-size: 24px;\nmargin-bottom: 12px;\n}\nbutton {\nbackground-color: #42b883;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #33a06f;\n}\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment</button>\n`;\nthis.shadowRoot.\nquerySelector(\"#increment\").\naddEventListener(\"click\", () => this.increment());\n}\ndisconnectedCallback() {\nconst button = this.shadowRoot.querySelector(\"#increment\");\nif (button) {\nbutton.removeEventListener(\"click\", () => this.increment());\n}\n}}\n// Register the Web Component\nif (!customElements.get(\"simple-counter\")) {\ncustomElements.define(\"simple-counter\", SimpleCounter);\n}\n// Vue App\nconst app = Vue.createApp({\ndata() {\nreturn {\ncounterData: 5,\nevents: [] };\n},\nmethods: {\nhandleCounterEvent(event) {\nthis.counterData = event.detail.count;\nthis.events.push(`Counter changed: ${event.detail.count}`);\n},\nincrementCounter() {\nthis.counterData++;\nthis.$nextTick(() => {\n// Vue's reactivity will update the attribute on the Web Component\n});\n},\ncallComponentMethod() {\n// Direct method call via ref\nthis.$refs.directCounter.increment();\nthis.events.push(\"Called increment() method directly\");\n} },\nmounted() {\n// Initial setup if needed\nthis.$refs.directCounter.count = 10;\n} });\napp.mount(\"#app\");\nVue + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nSvelte Integration Strategies\nSvelte treats Web Components as first-class citizens and makes integration particularly straightforward:\n<script>\nimport { onMount } from 'svelte';\nlet el;\nlet myData = { key: 'value' };\n// Reactive assignments automatically update the Web Component\n$: if (el) {\nel.complexData = myData;\n}\nfunction handleEvent(event) {\nconsole.log(event.detail);\n}\nonMount(() => {\n// If needed, do direct manipulation here\nel.addEventListener('special-event', specialHandler);\nreturn () => {\nel.removeEventListener('special-event', specialHandler);\n}\n});\n</script>\n<my-web-component\nbind:this={el}\non:custom-event={handleEvent}\n/>\nKey Insights:\nbind:this provides direct access to the element\nSvelte's reactivity automatically updates properties\nSvelte's event handling works natively with Web Component events\nInteractive Demo: Explore Svelte integration Techniques\nHTML\n<!-- The Svelte component below (App) is compiled in your browser and mounted here. -->\n<div id=\"app\"></div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\nbackground-color: #f8f9fa;\n}\n.demo-container {\nmargin: 20px 0;\n}\n.example {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 16px;\nmargin-bottom: 20px;\nbackground-color: #fff;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.05);\n}\nh2,\nh3 {\ncolor: #2c3e50;\nmargin-top: 0;\n}\n.button-group {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\nmargin-top: 8px;\n}\n.component-button,\n.svelte-button {\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\n.component-button { background-color: #3498db; }\n.component-button:hover { background-color: #2980b9; }\n.svelte-button { background-color: #ff3e00; }\n.svelte-button:hover { background-color: #d03400; }\n.events {\nmargin-top: 16px;\npadding: 12px;\nbackground-color: #f8f9fa;\nborder-radius: 4px;\nfont-size: 14px;\nborder: 1px solid #e9ecef;\nmax-height: 200px;\noverflow-y: auto;\n}\nJavaScript\n// 1. A framework-agnostic Web Component: a counter with a `count` property,\n//    an `increment()` method, and a `counter-changed` event.\nclass SimpleCounter extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._count = 0;\nthis.render();\n}\nstatic get observedAttributes() {\nreturn ['count'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'count') this.count = parseInt(newValue || '0', 10);\n}\nget count() {\nreturn this._count;\n}\nset count(value) {\nif (value === this._count) return;\nthis._count = value;\nthis.render();\nthis.dispatchEvent(new CustomEvent('counter-changed', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count }\n}));\n}\nincrement() {\nthis.count = this._count + 1;\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: sans-serif;\nborder: 2px solid #ff3e00;\nborder-radius: 8px;\npadding: 16px;\nmargin: 8px 0;\nbackground-color: #f8f9fa;\n}\n.counter { font-size: 24px; margin-bottom: 12px; }\nbutton {\nbackground-color: #ff3e00;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\n}\nbutton:hover { background-color: #d03400; }\n</style>\n<div class=\"counter\">Count: ${this._count}</div>\n<button id=\"increment\">Increment from Inside</button>\n`;\nthis.shadowRoot.querySelector('#increment').addEventListener('click', () => this.increment());\n}\n}\nif (!customElements.get('simple-counter')) {\ncustomElements.define('simple-counter', SimpleCounter);\n}\n// 2. A real Svelte component that uses it: bind:this for direct access,\n//    a reactive statement to push store values in, and on:counter-changed to listen.\nconst App = `\n<script>\nimport { onMount } from 'svelte';\nimport { writable } from 'svelte/store';\nconst count = writable(5);\nconst events = writable([]);\nlet counter;\n// Reactive statement: whenever $count changes, update the Web Component\n$: if (counter && counter.count !== $count) {\ncounter.count = $count;\n}\nfunction log(message) {\nevents.update((list) => [message, ...list].slice(0, 8));\n}\n// The Web Component's own event, handled with Svelte's on: directive\nfunction handleCounterChanged(event) {\ncount.set(event.detail.count);\nlog('counter-changed: ' + event.detail.count);\n}\nonMount(() => {\nlog('Mounted; bind:this gave us <' + counter.localName + '>');\n});\n</script>\n<div class=\"demo-container\">\n<h2>Svelte + Web Components Integration</h2>\n<p>A real Svelte component driving a Web Component with bind:this, a store, and its events</p>\n<div class=\"example\">\n<h3>Dual Approach Integration</h3>\n<p>Compare calling the Web Component's own method with updating a Svelte store</p>\n<simple-counter bind:this={counter} on:counter-changed={handleCounterChanged}></simple-counter>\n<div class=\"button-group\">\n<button class=\"component-button\" on:click={() => counter.increment()}>Increment (Component)</button>\n<button class=\"svelte-button\" on:click={() => count.update((n) => n + 1)}>Increment (Svelte Store)</button>\n</div>\n<div class=\"button-group\">\n<button class=\"component-button\" on:click={() => count.update((n) => Math.max(0, n - 1))}>Decrement</button>\n<button class=\"component-button\" on:click={() => count.set(0)}>Reset</button>\n<button class=\"component-button\" on:click={() => count.update((n) => n * 2)}>Double Value</button>\n</div>\n<div class=\"events\">\n<strong>Current count in the Svelte store: {$count}</strong>\n{#each $events as event, i (i)}\n<div>{event}</div>\n{/each}\n</div>\n</div>\n</div>\n`;\n// 3. Compile the component in the browser (as the Svelte REPL does) and mount it.\n//    `svelte` is the compiler; `SvelteRuntime` holds the modules compiled code imports.\nfunction load(source) {\nconst { js } = svelte.compile(source, { name: 'App', generate: 'client' });\nconst code = js.code\n.replace(/^import\\s+['\"][^'\"]+['\"];?$/gm, '')\n.replace(/^import\\s+\\*\\s+as\\s+([\\w$]+)\\s+from\\s+['\"]([^'\"]+)['\"];?$/gm, 'const $1 = runtime(\"$2\");')\n.replace(/^import\\s+\\{([^}]*)\\}\\s+from\\s+['\"]([^'\"]+)['\"];?$/gm,\n(_, names, from) => `const {${names.replace(/\\s+as\\s+/g, ': ')}} = runtime(\"${from}\");`)\n.replace(/^export default /m, 'const Component = ');\nconst runtime = (name) => {\nif (!(name in SvelteRuntime.modules)) throw new Error(`This sandbox can only import Svelte's own modules, not \"${name}\".`);\nreturn SvelteRuntime.modules[name];\n};\nreturn new Function('runtime', `${code}\\nreturn Component;`)(runtime);\n}\ntry {\nconst { mount } = SvelteRuntime.modules.svelte;\nmount(load(App), { target: document.getElementById('app') });\nconsole.log(`Compiled and mounted with Svelte ${svelte.VERSION}`);\n} catch (error) {\nconsole.error(error.message);\n}\nSvelte + Web Components Integration on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nPerformance Optimization Techniques\nLazy-Loading Strategies\nLoading Web Components only when needed can significantly improve initial page load performance:\nDynamic Import Pattern\n// Only load when needed\nconst loadComponent = async () => {\nconst { MyComponent } = await import('./my-component.js');\ncustomElements.define('my-component', MyComponent);\n}\n// Call on demand or when component is about to be needed\nbutton.addEventListener('click', loadComponent);\nIntersection Observer Pattern\n// Setup observer to load component when scrolled into view\nconst observer = new IntersectionObserver((entries) => {\nentries.forEach(entry => {\nif (entry.isIntersecting) {\nimport('./components/lazy-component.js')\n.then(() => {\n// Component is now registered\nobserver.unobserve(entry.target);\n});\n}\n});\n}, { rootMargin: '100px' });\n// Observe placeholder elements\ndocument.querySelectorAll('.component-placeholder')\n.forEach(el => observer.observe(el));\nInteractive Demo: See lazy-loading in action\nHTML\n<div class=\"demo-container\">\n<h1>Lazy-Loading Web Components</h1>\n<p>This demo shows how lazy-loading can improve performance by loading components only when needed.</p>\n<div class=\"performance-comparison\">\n<div class=\"tabs\">\n<button class=\"tab active\" data-tab=\"eager\">Eager Loading</button>\n<button class=\"tab\" data-tab=\"dynamic\">Dynamic Import</button>\n<button class=\"tab\" data-tab=\"intersection\">Intersection Observer</button>\n</div>\n<div class=\"tab-content\" id=\"eager-tab\">\n<h3>Eager Loading (All At Once)</h3>\n<p>All components are loaded immediately, which can slow down initial page load.</p>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Components Loaded:</div>\n<div class=\"metric-value\" id=\"eager-count\">0</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Initial Load Time:</div>\n<div class=\"metric-value\" id=\"eager-time\">-</div>\n</div>\n</div>\n<button class=\"demo-button\" id=\"load-eager\">Load 10 Eager Components</button>\n<div class=\"component-container\" id=\"eager-components\"></div>\n</div>\n<div class=\"tab-content hidden\" id=\"dynamic-tab\">\n<h3>Dynamic Import (On Demand)</h3>\n<p>Components are loaded on-click, improving initial page load but requiring user action.</p>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Components Loaded:</div>\n<div class=\"metric-value\" id=\"dynamic-count\">0</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Load Time:</div>\n<div class=\"metric-value\" id=\"dynamic-time\">-</div>\n</div>\n</div>\n<button class=\"demo-button\" id=\"load-dynamic\">Load Components On-Demand</button>\n<div class=\"component-container\" id=\"dynamic-components\"></div>\n</div>\n<div class=\"tab-content hidden\" id=\"intersection-tab\">\n<h3>Intersection Observer (Viewport-Based)</h3>\n<p>Components load automatically when scrolled into view - the best of both worlds.</p>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Components Visible:</div>\n<div class=\"metric-value\" id=\"intersection-count\">0</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Load Time per Component:</div>\n<div class=\"metric-value\" id=\"intersection-time\">-</div>\n</div>\n</div>\n<p>Scroll down to see components load as they enter the viewport:</p>\n<div class=\"component-container\" id=\"intersection-components\">\n<div class=\"component-placeholder\" data-index=\"1\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"2\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"3\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"4\">Component will load when visible</div>\n<div class=\"component-placeholder\" data-index=\"5\">Component will load when visible</div>\n</div>\n</div>\n</div>\n<div class=\"code-examples\">\n<h2>Implementation Examples</h2>\n<div class=\"code-block\">\n<h3>Dynamic Import Pattern</h3>\n<pre><code>// Only load when the user takes an action\nconst loadComponent = async () => {\nconst startTime = performance.now();\n// Dynamic import of the component module\nconst { MyComponent } = await import('./my-component.js');\n// Register the component only when needed\ncustomElements.define('my-component', MyComponent);\n// Create and add the component to the page\nconst component = document.createElement('my-component');\ndocument.querySelector('#container').appendChild(component);\nconst loadTime = performance.now() - startTime;\nconsole.log(`Component loaded in ${loadTime}ms`);\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h3>Intersection Observer Pattern</h3>\n<pre><code>// Create observer for viewport detection\nconst observer = new IntersectionObserver((entries) => {\nentries.forEach(entry => {\nif (entry.isIntersecting) {\nconst placeholder = entry.target;\n// Load component when placeholder is visible\nimport('./lazy-component.js')\n.then(module => {\n// Replace placeholder with actual component\nconst component = document.createElement('lazy-component');\nplaceholder.parentNode.replaceChild(component, placeholder);\n// Stop observing this element\nobserver.unobserve(placeholder);\n});\n}\n});\n}, { rootMargin: '100px' });\n// Start observing placeholders\ndocument.querySelectorAll('.component-placeholder')\n.forEach(el => observer.observe(el));</code></pre>\n</div>\n</div>\n</div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 900px;\nmargin: 0 auto;\npadding: 20px;\nline-height: 1.6;\ncolor: #333;\n}\nh1, h2, h3 {\ncolor: #2c3e50;\n}\n.demo-container {\nmargin: 20px 0;\n}\n.performance-comparison {\nborder: 1px solid #ddd;\nborder-radius: 8px;\noverflow: hidden;\nmargin: 30px 0;\n}\n.tabs {\ndisplay: flex;\nbackground-color: #f0f0f0;\nborder-bottom: 1px solid #ddd;\n}\n.tab {\npadding: 12px 20px;\nbackground: none;\nborder: none;\ncursor: pointer;\nfont-size: 16px;\nfont-weight: 500;\ncolor: #666;\ntransition: background-color 0.2s;\n}\n.tab:hover {\nbackground-color: #e0e0e0;\n}\n.tab.active {\nbackground-color: white;\ncolor: #2c3e50;\nborder-bottom: 3px solid #3498db;\n}\n.tab-content {\npadding: 20px;\nbackground-color: white;\n}\n.hidden {\ndisplay: none;\n}\n.metrics {\ndisplay: flex;\ngap: 20px;\nmargin: 20px 0;\npadding: 15px;\nbackground-color: #f8f9fa;\nborder-radius: 8px;\n}\n.metric {\nflex: 1;\n}\n.metric-title {\nfont-weight: bold;\nmargin-bottom: 5px;\ncolor: #666;\n}\n.metric-value {\nfont-size: 24px;\ncolor: #3498db;\n}\n.demo-button {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 10px 20px;\nfont-size: 16px;\ncursor: pointer;\ntransition: background-color 0.2s;\n}\n.demo-button:hover {\nbackground-color: #2980b9;\n}\n.component-container {\nmargin-top: 20px;\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\ngap: 15px;\n}\n.component-placeholder {\nheight: 150px;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\ntext-align: center;\nborder: 2px dashed #ccc;\nborder-radius: 8px;\nbackground-color: #f5f5f5;\ncolor: #777;\npadding: 10px;\n}\n.code-examples {\nmargin-top: 40px;\n}\n.code-block {\nmargin: 20px 0;\n}\n.code-block pre {\nbackground-color: #f5f5f5;\npadding: 15px;\nborder-radius: 8px;\noverflow-x: auto;\nborder: 1px solid #ddd;\n}\n.code-block code {\nfont-family: 'Consolas', 'Monaco', 'Courier New', monospace;\nfont-size: 14px;\nline-height: 1.5;\n}\nJavaScript\n// HTML\n// Define fancy component to be loaded\nclass FancyComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._rendered = false;\nthis._index = this.getAttribute('data-index') || '?';\nthis._loadTime = this.getAttribute('data-load-time') || '0';\nthis.render();\n}\nconnectedCallback() {\nif (!this._rendered) {\nthis.render();\n}\n// Simulate real-world component initialization work\nthis.simulateWork();\n}\nsimulateWork() {\n// CPU-intensive operation to simulate real component work\nlet result = 0;\nfor (let i = 0; i < 100000; i++) {\nresult += Math.sqrt(i) * Math.random();\n}\nthis.shadowRoot.querySelector('.value').textContent = result.toFixed(0);\n}\nrender() {\nthis._rendered = true;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nborder-radius: 8px;\noverflow: hidden;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\nbackground-color: white;\ntransition: transform 0.2s;\n}\n:host(:hover) {\ntransform: translateY(-5px);\n}\n.header {\nbackground-color: #3498db;\ncolor: white;\npadding: 12px;\nfont-weight: bold;\ntext-align: center;\n}\n.body {\npadding: 15px;\ntext-align: center;\n}\n.info {\ndisplay: flex;\njustify-content: space-between;\nmargin-top: 15px;\nfont-size: 12px;\ncolor: #777;\n}\n.value {\nfont-size: 24px;\ncolor: #2c3e50;\nmargin: 10px 0;\n}\n</style>\n<div class=\"header\">Component #${this._index}</div>\n<div class=\"body\">\n<div>Calculated Value:</div>\n<div class=\"value\">Calculating...</div>\n<div class=\"info\">\n<span>Load time: ${this._loadTime}ms</span>\n<span>Memory: ~${(Math.random() * 5 + 1).toFixed(1)}MB</span>\n</div>\n</div>\n`;\n}}\n// Initialize tabs\nconst tabs = document.querySelectorAll('.tab');\ntabs.forEach(tab => {\ntab.addEventListener('click', () => {\n// Deactivate all tabs\ntabs.forEach(t => t.classList.remove('active'));\n// Hide all content\ndocument.querySelectorAll('.tab-content').forEach(content => {\ncontent.classList.add('hidden');\n});\n// Activate clicked tab\ntab.classList.add('active');\n// Show corresponding content\nconst tabId = tab.getAttribute('data-tab');\ndocument.getElementById(`${tabId}-tab`).classList.remove('hidden');\n});\n});\n// Register the component for eager loading\ncustomElements.define('fancy-component', FancyComponent);\n// Eager loading demo\ndocument.getElementById('load-eager').addEventListener('click', () => {\nconst container = document.getElementById('eager-components');\ncontainer.innerHTML = '';\nconst startTime = performance.now();\n// Create 10 components at once\nfor (let i = 1; i <= 10; i++) {\nconst component = document.createElement('fancy-component');\ncomponent.setAttribute('data-index', i);\ncontainer.appendChild(component);\n}\nconst totalTime = performance.now() - startTime;\n// Update metrics\ndocument.getElementById('eager-count').textContent = '10';\ndocument.getElementById('eager-time').textContent = `${totalTime.toFixed(1)}ms`;\n});\n// Dynamic import demo\ndocument.getElementById('load-dynamic').addEventListener('click', async () => {\nconst container = document.getElementById('dynamic-components');\ncontainer.innerHTML = '';\nconst startTime = performance.now();\n// Load components one by one with a small delay to better see the process\nfor (let i = 1; i <= 5; i++) {\nconst componentStartTime = performance.now();\n// Simulate dynamic import (we already have the component)\nawait new Promise(resolve => setTimeout(resolve, 200));\nconst component = document.createElement('fancy-component');\ncomponent.setAttribute('data-index', i);\ncomponent.setAttribute('data-load-time', (performance.now() - componentStartTime).toFixed(1));\ncontainer.appendChild(component);\n// Update count\ndocument.getElementById('dynamic-count').textContent = i;\n}\nconst totalTime = performance.now() - startTime;\ndocument.getElementById('dynamic-time').textContent = `${totalTime.toFixed(1)}ms`;\n});\n// Intersection Observer demo\nconst observerOptions = {\nrootMargin: '0px',\nthreshold: 0.1 };\nconst componentObserver = new IntersectionObserver(entries => {\nentries.forEach(entry => {\nif (entry.isIntersecting) {\nconst placeholder = entry.target;\nconst index = placeholder.getAttribute('data-index');\n// Measure loading time\nconst startTime = performance.now();\n// Simulate dynamic import\nsetTimeout(() => {\nconst component = document.createElement('fancy-component');\ncomponent.setAttribute('data-index', index);\nconst loadTime = performance.now() - startTime;\ncomponent.setAttribute('data-load-time', loadTime.toFixed(1));\n// Replace placeholder with actual component\nplaceholder.parentNode.replaceChild(component, placeholder);\n// Update metrics\nconst countEl = document.getElementById('intersection-count');\ncountEl.textContent = parseInt(countEl.textContent || '0') + 1;\ndocument.getElementById('intersection-time').textContent = `${loadTime.toFixed(1)}ms`;\n}, 300); // Simulated import delay\n// Stop observing this element\ncomponentObserver.unobserve(placeholder);\n}\n});\n}, observerOptions);\n// Start observing placeholders\ndocument.querySelectorAll('.component-placeholder').forEach(placeholder => {\ncomponentObserver.observe(placeholder);\n});\n// Initialize counters\ndocument.getElementById('eager-count').textContent = '0';\ndocument.getElementById('dynamic-count').textContent = '0';\ndocument.getElementById('intersection-count').textContent = '0';\nLazy-Loading Web Components on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nShadow DOM Performance\nThe Shadow DOM provides encapsulation but comes with performance considerations:\nContainment for Performance\n:host {\ncontain: content; /* Limit style recalculation scope */\ncontainer-type: inline-size; /* For container queries */\n}\nMinimizing Selectors\n/* Avoid overly complex selectors that cross shadow boundaries */\n:host ::slotted(*) > * { /* Expensive */ }\n/* Better to use direct, simple selectors */\n.my-item { /* More efficient */ }\nSlot Change Optimization\n// Listen only when needed\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' }).innerHTML = '<slot></slot>';\nthis.shadowRoot.querySelector('slot')\n.addEventListener('slotchange', this._handleSlotChange);\n}\n// Debounce frequent updates\n_handleSlotChange = debounce(() => {\nthis._processSlottedChildren();\n}, 100);\nRender Optimization\nEfficient rendering patterns can dramatically improve performance:\nBatched DOM Updates\n// Poor: Multiple separate DOM operations\nthis.shadowRoot.querySelector('.title').textContent = title;\nthis.shadowRoot.querySelector('.desc').textContent = description;\n// Better: Batch DOM operations with DocumentFragment\nupdate(data) {\nconst fragment = document.createDocumentFragment();\n// Build complete update in memory\nconst title = document.createElement('div');\ntitle.className = 'title';\ntitle.textContent = data.title;\nfragment.appendChild(title);\n// etc...\n// Single DOM operation\nconst container = this.shadowRoot.querySelector('.container');\ncontainer.innerHTML = '';\ncontainer.appendChild(fragment);\n}\nRequestAnimationFrame for Visual Updates\nupdateVisuals() {\n// Schedule visual updates in animation frame\nrequestAnimationFrame(() => {\nthis.elements.forEach(el => {\nel.style.transform = `translate(${this.x}px, ${this.y}px)`;\n});\n});\n}\nInteractive Demo: Compare optimized vs. unoptimized components\nHTML\n<div class=\"demo-container\">\n<h1>Web Component Render Optimization</h1>\n<p>Compare optimized vs. unoptimized rendering approaches in Web Components</p>\n<div class=\"tabs\">\n<button class=\"tab active\" data-tab=\"basic\">Basic Comparison</button>\n<button class=\"tab\" data-tab=\"techniques\">Optimization Techniques</button>\n</div>\n<div class=\"tab-content\" id=\"basic-tab\">\n<h2>Performance Comparison</h2>\n<p>Watch the difference between optimized and unoptimized component updates when handling frequent state changes.</p>\n<div class=\"control-panel\">\n<div class=\"control\">\n<label for=\"update-frequency\">Update Frequency:</label>\n<select id=\"update-frequency\">\n<option value=\"500\">Low (500ms)</option>\n<option value=\"100\">Medium (100ms)</option>\n<option value=\"16\" selected>High (16ms)</option>\n</select>\n</div>\n<div class=\"control\">\n<label for=\"item-count\">Item Count:</label>\n<select id=\"item-count\">\n<option value=\"10\">Few (10)</option>\n<option value=\"50\" selected>Medium (50)</option>\n<option value=\"100\">Many (100)</option>\n</select>\n</div>\n<button id=\"start-test\" class=\"demo-button\">Start Comparison</button>\n<button id=\"stop-test\" class=\"demo-button\" disabled>Stop</button>\n</div>\n<div class=\"performance-results\">\n<div class=\"result-box\">\n<h3>Unoptimized Component</h3>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Frame Rate:</div>\n<div class=\"metric-value\" id=\"unoptimized-fps\">0 FPS</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Updates:</div>\n<div class=\"metric-value\" id=\"unoptimized-updates\">0</div>\n</div>\n</div>\n<div id=\"unoptimized-container\" class=\"component-container\">\n<div class=\"placeholder\">Click \"Start Comparison\" to initialize</div>\n</div>\n</div>\n<div class=\"result-box\">\n<h3>Optimized Component</h3>\n<div class=\"metrics\">\n<div class=\"metric\">\n<div class=\"metric-title\">Frame Rate:</div>\n<div class=\"metric-value\" id=\"optimized-fps\">0 FPS</div>\n</div>\n<div class=\"metric\">\n<div class=\"metric-title\">Updates:</div>\n<div class=\"metric-value\" id=\"optimized-updates\">0</div>\n</div>\n</div>\n<div id=\"optimized-container\" class=\"component-container\">\n<div class=\"placeholder\">Click \"Start Comparison\" to initialize</div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"tab-content hidden\" id=\"techniques-tab\">\n<h2>Optimization Techniques</h2>\n<div class=\"technique\">\n<h3>1. Batched DOM Updates</h3>\n<p>Instead of updating the DOM multiple times, batch changes into a single update using DocumentFragment.</p>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Unoptimized Approach</h4>\n<pre><code>// Unoptimized: Multiple separate DOM operations\nupdateItems(items) {\n// Clear existing items\nthis.listElement.innerHTML = '';\n// Add each item individually (multiple DOM operations)\nitems.forEach(item => {\nconst li = document.createElement('li');\nli.textContent = item.text;\nthis.listElement.appendChild(li);\n});\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Optimized Approach</h4>\n<pre><code>// Optimized: Single DOM operation with DocumentFragment\nupdateItems(items) {\n// Create fragment (in-memory container)\nconst fragment = document.createDocumentFragment();\n// Build complete structure in memory\nitems.forEach(item => {\nconst li = document.createElement('li');\nli.textContent = item.text;\nfragment.appendChild(li);\n});\n// Single DOM operation\nthis.listElement.innerHTML = '';\nthis.listElement.appendChild(fragment);\n}</code></pre>\n</div>\n</div>\n</div>\n<div class=\"technique\">\n<h3>2. RequestAnimationFrame for Visual Updates</h3>\n<p>Schedule visual updates to align with the browser's render cycle for smoother animations.</p>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Unoptimized Approach</h4>\n<pre><code>// Unoptimized: Direct style changes\nupdatePosition(x, y) {\n// This might cause layout thrashing if called frequently\nthis.element.style.transform = `translate(${x}px, ${y}px)`;\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Optimized Approach</h4>\n<pre><code>// Optimized: RAF for smooth visual updates\nupdatePosition(x, y) {\n// Store the values to update\nthis._pendingX = x;\nthis._pendingY = y;\n// Only schedule update if none is pending\nif (!this._updateScheduled) {\nthis._updateScheduled = true;\nrequestAnimationFrame(() => {\n// Apply all pending updates at once\nthis.element.style.transform =\n`translate(${this._pendingX}px, ${this._pendingY}px)`;\nthis._updateScheduled = false;\n});\n}\n}</code></pre>\n</div>\n</div>\n</div>\n<div class=\"technique\">\n<h3>3. Debouncing Property Changes</h3>\n<p>Limit the frequency of updates when properties change rapidly.</p>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Unoptimized Approach</h4>\n<pre><code>// Unoptimized: Update on every property change\nset value(newValue) {\nthis._value = newValue;\nthis.render(); // May be called too frequently\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Optimized Approach</h4>\n<pre><code>// Optimized: Debounced updates\nconstructor() {\nsuper();\nthis._value = 0;\nthis._debounceTimeout = null;\n}\nset value(newValue) {\nthis._value = newValue;\n// Clear previous timeout\nclearTimeout(this._debounceTimeout);\n// Schedule a new update with a delay\nthis._debounceTimeout = setTimeout(() => {\nthis.render();\n}, 16); // Roughly one frame at 60fps\n}</code></pre>\n</div>\n</div>\n</div>\n</div>\n</div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\nmax-width: 1200px;\nmargin: 0 auto;\npadding: 20px;\nline-height: 1.6;\ncolor: #333;\nbackground-color: #f8f9fa;\n}\nh1, h2, h3, h4 {\ncolor: #2c3e50;\n}\n.demo-container {\nmargin: 20px 0;\n}\n.tabs {\ndisplay: flex;\ngap: 2px;\nmargin-bottom: 20px;\n}\n.tab {\npadding: 10px 20px;\nbackground-color: #e9ecef;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\n.tab:hover {\nbackground-color: #dee2e6;\n}\n.tab.active {\nbackground-color: #3498db;\ncolor: white;\n}\n.tab-content {\nbackground-color: white;\npadding: 20px;\nborder-radius: 8px;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n.hidden {\ndisplay: none;\n}\n.control-panel {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 15px;\nalign-items: center;\nbackground-color: #f0f7ff;\npadding: 15px;\nborder-radius: 8px;\nmargin-bottom: 20px;\n}\n.control {\ndisplay: flex;\nalign-items: center;\ngap: 8px;\n}\nlabel {\nfont-weight: 500;\n}\nselect {\npadding: 8px;\nborder-radius: 4px;\nborder: 1px solid #ced4da;\nbackground-color: white;\nfont-size: 16px;\n}\n.demo-button {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.2s;\n}\n.demo-button:hover {\nbackground-color: #2980b9;\n}\n.demo-button:disabled {\nbackground-color: #95a5a6;\ncursor: not-allowed;\n}\n.performance-results {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 20px;\n}\n.result-box {\nbackground-color: white;\npadding: 20px;\nborder-radius: 8px;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n.metrics {\ndisplay: flex;\ngap: 20px;\nmargin: 15px 0;\npadding: 15px;\nbackground-color: #f8f9fa;\nborder-radius: 8px;\n}\n.metric {\nflex: 1;\n}\n.metric-title {\nfont-weight: bold;\nmargin-bottom: 5px;\ncolor: #666;\n}\n.metric-value {\nfont-size: 24px;\ncolor: #3498db;\n}\n.component-container {\nborder: 1px solid #e9ecef;\nborder-radius: 8px;\npadding: 15px;\nheight: 300px;\noverflow: auto;\nbackground-color: #f8f9fa;\n}\n.placeholder {\ndisplay: flex;\nheight: 100%;\nalign-items: center;\njustify-content: center;\ncolor: #7f8c8d;\nfont-style: italic;\n}\n.technique {\nmargin-bottom: 40px;\npadding-bottom: 20px;\nborder-bottom: 1px solid #e9ecef;\n}\n.code-comparison {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 20px;\nmargin-top: 15px;\n}\n.code-block {\nbackground-color: #f8f9fa;\nborder-radius: 8px;\noverflow: hidden;\nborder: 1px solid #e9ecef;\n}\n.code-block h4 {\nmargin: 0;\npadding: 10px 15px;\nbackground-color: #e9ecef;\ncolor: #2c3e50;\n}\n.code-block pre {\nmargin: 0;\npadding: 15px;\noverflow-x: auto;\n}\n.code-block code {\nfont-family: 'Consolas', 'Monaco', 'Courier New', monospace;\n}\n/* Mobile responsiveness */\n@media (max-width: 768px) {\n.performance-results, .code-comparison {\ngrid-template-columns: 1fr;\n}\n}\nJavaScript\nclass UnoptimizedGrid extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.items = [];\nthis.updateCount = 0;\nthis.lastFrameTime = 0;\nthis.frameCount = 0;\nthis.fps = 0;\n// Initialize the component\nthis.render();\n}\n// Called when the element is added to the document\nconnectedCallback() {\n// Start FPS counter\nthis.startFpsCounter();\n}\n// Called when the element is removed from the document\ndisconnectedCallback() {\n// Clean up\nthis.stopFpsCounter();\n}\n// Method to update the data items\nupdateData(items) {\nthis.items = items;\nthis.updateCount++;\n// Update the metrics display\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n// Render immediately - this is unoptimized\nthis.render();\n}\n// Unoptimized render method - rebuilds everything on each update\nrender() {\n// Create the basic structure if it doesn't exist\nif (!this.shadowRoot.querySelector('.grid')) {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 100%;\n}\n.grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\ngap: 10px;\npadding: 5px;\n}\n.grid-item {\nbackground-color: #3498db;\ncolor: white;\npadding: 10px;\nborder-radius: 4px;\ntext-align: center;\ntransition: background-color 0.2s;\n}\n.grid-item:hover {\nbackground-color: #2980b9;\n}\n</style>\n<div class=\"grid\"></div>\n`;\n}\nconst grid = this.shadowRoot.querySelector('.grid');\n// Unoptimized: Clear and rebuild everything\ngrid.innerHTML = '';\n// Unoptimized: Add each item individually (causes multiple reflows)\nthis.items.forEach(item => {\nconst itemEl = document.createElement('div');\nitemEl.className = 'grid-item';\nitemEl.textContent = item.value;\ngrid.appendChild(itemEl);\n});\n}\n// FPS tracking methods\nstartFpsCounter() {\nthis.frameCount = 0;\nthis.lastFrameTime = performance.now();\nthis.fpsInterval = setInterval(() => {\nconst now = performance.now();\nconst delta = now - this.lastFrameTime;\nthis.fps = Math.round(this.frameCount * 1000 / delta);\n// Reset for next calculation\nthis.frameCount = 0;\nthis.lastFrameTime = now;\n// Dispatch metrics update event\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n}, 1000);\n}\nstopFpsCounter() {\nclearInterval(this.fpsInterval);\n}\n// Called for each frame render\ncountFrame() {\nthis.frameCount++;\n}}\n// Optimized Data Grid Component\nclass OptimizedGrid extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.items = [];\nthis.updateCount = 0;\nthis.lastFrameTime = 0;\nthis.frameCount = 0;\nthis.fps = 0;\n// Flag to track if a render has been scheduled\nthis._updatePending = false;\n// Initialize the component\nthis.render();\n}\n// Called when the element is added to the document\nconnectedCallback() {\n// Start FPS counter\nthis.startFpsCounter();\n}\n// Called when the element is removed from the document\ndisconnectedCallback() {\n// Clean up\nthis.stopFpsCounter();\n}\n// Method to update the data items\nupdateData(items) {\nthis.items = items;\nthis.updateCount++;\n// Update the metrics display\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n// Optimized: Schedule a render using requestAnimationFrame\nthis.scheduleRender();\n}\n// Optimization 1: Schedule renders for better performance\nscheduleRender() {\n// Only schedule a new render if one isn't already pending\nif (!this._updatePending) {\nthis._updatePending = true;\nrequestAnimationFrame(() => {\nthis.render();\nthis._updatePending = false;\n});\n}\n}\n// Optimized render method - uses DocumentFragment for batch updates\nrender() {\n// Create the basic structure if it doesn't exist\nif (!this.shadowRoot.querySelector('.grid')) {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 100%;\n}\n.grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(80px, 1fr));\ngap: 10px;\npadding: 5px;\n}\n.grid-item {\nbackground-color: #3498db;\ncolor: white;\npadding: 10px;\nborder-radius: 4px;\ntext-align: center;\ntransition: background-color 0.2s;\n}\n.grid-item:hover {\nbackground-color: #2980b9;\n}\n</style>\n<div class=\"grid\"></div>\n`;\n}\nconst grid = this.shadowRoot.querySelector('.grid');\n// Optimization 2: Use DocumentFragment for batched DOM updates\nconst fragment = document.createDocumentFragment();\n// Build all elements in memory first\nthis.items.forEach(item => {\nconst itemEl = document.createElement('div');\nitemEl.className = 'grid-item';\nitemEl.textContent = item.value;\nfragment.appendChild(itemEl);\n});\n// Optimization 3: Single DOM operation\ngrid.innerHTML = '';\ngrid.appendChild(fragment);\n}\n// FPS tracking methods\nstartFpsCounter() {\nthis.frameCount = 0;\nthis.lastFrameTime = performance.now();\nthis.fpsInterval = setInterval(() => {\nconst now = performance.now();\nconst delta = now - this.lastFrameTime;\nthis.fps = Math.round(this.frameCount * 1000 / delta);\n// Reset for next calculation\nthis.frameCount = 0;\nthis.lastFrameTime = now;\n// Dispatch metrics update event\nthis.dispatchEvent(new CustomEvent('metrics-update', {\ndetail: {\nupdates: this.updateCount,\nfps: this.fps } }));\n}, 1000);\n}\nstopFpsCounter() {\nclearInterval(this.fpsInterval);\n}\n// Called for each frame render\ncountFrame() {\nthis.frameCount++;\n}}\n// Register our Web Components\ncustomElements.define('unoptimized-grid', UnoptimizedGrid);\ncustomElements.define('optimized-grid', OptimizedGrid);\n// Initialize demo\ndocument.addEventListener('DOMContentLoaded', () => {\n// Setup tab navigation\nconst tabs = document.querySelectorAll('.tab');\ntabs.forEach(tab => {\ntab.addEventListener('click', () => {\n// Deactivate all tabs\ntabs.forEach(t => t.classList.remove('active'));\n// Hide all content\ndocument.querySelectorAll('.tab-content').forEach(content => {\ncontent.classList.add('hidden');\n});\n// Activate clicked tab\ntab.classList.add('active');\n// Show corresponding content\nconst tabId = tab.getAttribute('data-tab');\ndocument.getElementById(`${tabId}-tab`).classList.remove('hidden');\n});\n});\nlet unoptimizedGrid;\nlet optimizedGrid;\nlet updateInterval;\nlet animationFrameId;\n// Performance test controls\nconst startButton = document.getElementById('start-test');\nconst stopButton = document.getElementById('stop-test');\nconst frequencySelect = document.getElementById('update-frequency');\nconst itemCountSelect = document.getElementById('item-count');\n// Function to generate random test data\nfunction generateTestData(count) {\nconst data = [];\nfor (let i = 0; i < count; i++) {\ndata.push({\nid: i,\nvalue: Math.floor(Math.random() * 100) });\n}\nreturn data;\n}\n// Update metrics displays\nfunction setupMetricsListeners() {\nunoptimizedGrid.addEventListener('metrics-update', e => {\ndocument.getElementById('unoptimized-fps').textContent = `${e.detail.fps} FPS`;\ndocument.getElementById('unoptimized-updates').textContent = e.detail.updates;\n});\noptimizedGrid.addEventListener('metrics-update', e => {\ndocument.getElementById('optimized-fps').textContent = `${e.detail.fps} FPS`;\ndocument.getElementById('optimized-updates').textContent = e.detail.updates;\n});\n}\n// Animation frame counter\nfunction countFrames() {\nunoptimizedGrid?.countFrame();\noptimizedGrid?.countFrame();\nanimationFrameId = requestAnimationFrame(countFrames);\n}\n// Start test button\nstartButton.addEventListener('click', () => {\n// Create components if they don't exist\nconst unoptimizedContainer = document.getElementById('unoptimized-container');\nconst optimizedContainer = document.getElementById('optimized-container');\n// Clear containers\nunoptimizedContainer.innerHTML = '';\noptimizedContainer.innerHTML = '';\n// Create components\nunoptimizedGrid = document.createElement('unoptimized-grid');\noptimizedGrid = document.createElement('optimized-grid');\n// Add to DOM\nunoptimizedContainer.appendChild(unoptimizedGrid);\noptimizedContainer.appendChild(optimizedGrid);\n// Setup metrics listeners\nsetupMetricsListeners();\n// Start frame counter\nanimationFrameId = requestAnimationFrame(countFrames);\n// Get update frequency and item count\nconst updateFrequency = parseInt(frequencySelect.value);\nconst itemCount = parseInt(itemCountSelect.value);\n// Start the update interval\nupdateInterval = setInterval(() => {\n// Generate new random data\nconst data = generateTestData(itemCount);\n// Update both components\nunoptimizedGrid.updateData(data);\noptimizedGrid.updateData(data);\n}, updateFrequency);\n// Update button states\nstartButton.disabled = true;\nstopButton.disabled = false;\nfrequencySelect.disabled = true;\nitemCountSelect.disabled = true;\n});\n// Stop test button\nstopButton.addEventListener('click', () => {\n// Clear the update interval\nclearInterval(updateInterval);\ncancelAnimationFrame(animationFrameId);\n// Update button states\nstartButton.disabled = false;\nstopButton.disabled = true;\nfrequencySelect.disabled = false;\nitemCountSelect.disabled = false;\n});\n});\nWeb Component Render Optimization on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/).\nAdvanced Patterns and Best Practices\nState Management Across Boundaries\nManaging state between Web Components and frameworks requires thoughtful patterns:\n1. The Context Pattern\nCreate a shared context accessible to all components:\n// context.js - Framework agnostic state management\nexport class ComponentContext {\nconstructor() {\nthis._data = {};\nthis._listeners = new Map();\n}\nget(key) {\nreturn this._data[key];\n}\nset(key, value) {\nthis._data[key] = value;\nif (this._listeners.has(key)) {\nthis._listeners.get(key).forEach(callback => callback(value));\n}\n}\nsubscribe(key, callback) {\nif (!this._listeners.has(key)) {\nthis._listeners.set(key, new Set());\n}\nthis._listeners.get(key).add(callback);\nreturn () => {\nthis._listeners.get(key).delete(callback);\n};\n}\n}\n// Create shared instance\nexport const context = new ComponentContext();\nUsage in components:\nimport { context } from './context.js';\nclass MyComponent extends HTMLElement {\nconnectedCallback() {\n// Subscribe to state changes\nthis._unsubscribe = context.subscribe('userData',\n(data) => this._updateFromState(data));\n// Initial state\nthis._updateFromState(context.get('userData'));\n}\ndisconnectedCallback() {\n// Clean up subscription\nthis._unsubscribe();\n}\n_updateState(data) {\n// Update shared state\ncontext.set('userData', data);\n}\n}\n2. The Message Bus Pattern\nFor loosely coupled components, a pub/sub event bus provides flexible communication:\n// event-bus.js\nexport class EventBus {\nconstructor() {\nthis.listeners = {};\n}\non(event, callback) {\nif (!this.listeners[event]) {\nthis.listeners[event] = [];\n}\nthis.listeners[event].push(callback);\nreturn () => this.off(event, callback);\n}\noff(event, callback) {\nif (this.listeners[event]) {\nthis.listeners[event] = this.listeners[event]\n.filter(cb => cb !== callback);\n}\n}\nemit(event, data) {\nif (this.listeners[event]) {\nthis.listeners[event].forEach(callback => {\ncallback(data);\n});\n}\n}\n}\n// Shared instance\nexport const eventBus = new EventBus();\nServer-Side Rendering with Web Components\nServer-side rendering improves initial load performance. With Declarative Shadow DOM (<template shadowrootmode=\"open\">), the server can send a component's shadow tree as plain HTML:\n<!-- Server-rendered output -->\n<my-component>\n<template shadowrootmode=\"open\">\n<style>\n/* Shadow DOM styles */\n.card {\npadding: 16px;\nborder: 1px solid #ddd;\n}\n</style>\n<div class=\"card\">\n<slot></slot>\n</div>\n</template>\n<p>This content is slotted from light DOM</p>\n</my-component>\n<!-- Polyfill for browsers without Declarative Shadow DOM -->\n<script>\nif (!HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode')) {\ndocument.querySelectorAll('template[shadowrootmode]').forEach(template => {\nconst mode = template.getAttribute('shadowrootmode');\nconst shadowRoot = template.parentNode.attachShadow({ mode });\nshadowRoot.appendChild(template.content);\ntemplate.remove();\n});\n}\n</script>\nTesting Strategies\nTesting Web Components requires specialized approaches:\n1. Component Unit Testing\n// Using web-test-runner and @open-wc/testing\nimport { html, fixture, expect } from '@open-wc/testing';\nimport '../src/my-component.js';\ndescribe('MyComponent', () => {\nit('renders with default values', async () => {\nconst el = await fixture(html`<my-component></my-component>`);\nexpect(el.shadowRoot.querySelector('.title').textContent)\n.to.equal('Default Title');\n});\nit('updates when properties change', async () => {\nconst el = await fixture(html`<my-component></my-component>`);\nel.title = 'New Title';\nawait el.updateComplete; // For LitElement components\nexpect(el.shadowRoot.querySelector('.title').textContent)\n.to.equal('New Title');\n});\nit('fires custom events', async () => {\nconst el = await fixture(html`<my-component></my-component>`);\n// Setup event listener\nlet eventFired = false;\nel.addEventListener('custom-event', () => eventFired = true);\n// Trigger event\nel.shadowRoot.querySelector('button').click();\nexpect(eventFired).to.be.true;\n});\n});\n2. Integration Testing with Frameworks\n// React Testing Library example\nimport { render, fireEvent, screen } from '@testing-library/react';\nimport MyReactComponent from '../src/MyReactComponent';\n// Register Web Component if not done globally\nimport '../src/web-components/my-element.js';\ntest('React component interacts with Web Component', async () => {\nrender(<MyReactComponent initialData=\"test\" />);\n// Interact with the wrapped Web Component\nfireEvent.click(screen.getByRole('button'));\n// Check the result\nexpect(screen.getByText('Success')).toBeInTheDocument();\n});\nConclusion\nYou can now bridge a framework and a web component:\nPass objects and arrays as properties (through a ref in React), not as attributes.\nListen for custom events with addEventListener, and remove the listener in the effect's cleanup.\nWrap that once in a small component so the rest of your app never thinks about it.\nA framework can now hand a component an object and hear its events, which was the whole problem.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-04-13T15:21:21Z",
      "id": "https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/",
      "image": "https://mnixblog.netlify.app/images/advanced-patterns-and-integration-with-frameworks/cover-v4.jpg",
      "summary": "Pass objects from React to a custom element without getting [object Object], and handle events across the framework boundary.",
      "tags": [
        "Web Components",
        "Frameworks"
      ],
      "title": "Advanced Patterns & Integration with Frameworks",
      "url": "https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/"
    },
    {
      "content_html": "<p>In today's diverse front-end landscape, Web Components stand out as a platform-native solution for creating reusable UI elements. While frameworks come and go, Web Components offer a standardized approach that works across codebases and technologies. But where can we see them in action? Who's using them in production?</p>\n<p>This post shows who publishes components such as <code>&lt;user-card&gt;</code>, and the four habits (composition, property against attribute, events, consistent names) that make them pleasant to use.</p>\n<p><strong>Who ships them:</strong> the clearest public evidence is open-source design systems and libraries you can read yourself. Google maintains <a href=\"https://lit.dev/\">Lit</a>, Adobe publishes <a href=\"https://opensource.adobe.com/spectrum-web-components/\">Spectrum Web Components</a>, Microsoft publishes <a href=\"https://www.fast.design/\">FAST</a> and Fluent UI web components, IBM's Carbon design system has a web components package, and Salesforce's Lightning Web Components are built on the same standards. This post names only what is publicly documented and makes no claims about closed-source internals.</p>\n<p>Recap if you are joining here: a custom element is a JavaScript class registered with <code>customElements.define()</code>, usually with a shadow root that keeps its styles private. Let's start with a basic example. This demo shows the lifecycle methods: you can see how the component reacts when it's added to the DOM, removed, or when its attributes change:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Basic Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Basic Web Component Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;controls&quot;&gt;\n    &lt;label for=&quot;name-input&quot;&gt;Change the name:&lt;/label&gt;\n    &lt;input id=&quot;name-input&quot; type=&quot;text&quot; value=&quot;World&quot;&gt;\n    &lt;button id=&quot;update-btn&quot;&gt;Update&lt;/button&gt;\n    &lt;button id=&quot;add-btn&quot;&gt;Add to DOM&lt;/button&gt;\n    &lt;button id=&quot;remove-btn&quot;&gt;Remove from DOM&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;component-container&quot; id=&quot;component-container&quot;&gt;\n    &lt;!-- The component will be added/removed here --&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;lifecycle-log&quot;&gt;\n    &lt;h3&gt;Lifecycle Events:&lt;/h3&gt;\n    &lt;div id=&quot;lifecycle-events&quot;&gt;&lt;/div&gt;\n    &lt;button id=&quot;clear-log&quot;&gt;Clear Log&lt;/button&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.container {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n  max-width: 600px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n.controls {\n  margin-bottom: 20px;\n  padding: 15px;\n  background-color: #f5f5f5;\n  border-radius: 5px;\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n  align-items: center;\n}\n\n.component-container {\n  border: 2px dashed #ccc;\n  padding: 20px;\n  min-height: 100px;\n  margin-bottom: 20px;\n  border-radius: 5px;\n}\n\n.lifecycle-log {\n  background-color: #f8f8f8;\n  padding: 15px;\n  border-radius: 5px;\n}\n\n#lifecycle-events {\n  max-height: 200px;\n  overflow-y: auto;\n  margin-bottom: 10px;\n  font-family: monospace;\n  font-size: 14px;\n  padding: 10px;\n  background-color: #eee;\n  border-radius: 3px;\n}\n\n.log-entry {\n  margin-bottom: 5px;\n  padding: 3px;\n  border-bottom: 1px solid #ddd;\n}\n\n.log-entry.constructor { color: #0066cc; }\n.log-entry.connected { color: #28a745; }\n.log-entry.disconnected { color: #dc3545; }\n.log-entry.attribute-changed { color: #fd7e14; }\n\nbutton {\n  padding: 8px 12px;\n  background-color: #4a6cf7;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 14px;\n}\n\nbutton:hover {\n  background-color: #3a5cf7;\n}\n\n#clear-log {\n  background-color: #6c757d;\n}\n\ninput {\n  padding: 8px;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n  font-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the custom element\nclass HelloWorld extends HTMLElement {\n  // Specify observed attributes for attributeChangedCallback\n  static get observedAttributes() {\n    return [&#39;name&#39;];\n  }\n\n  constructor() {\n    // Always call super() first in constructor\n    super();\n\n    // Create a shadow root\n    const shadow = this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create element contents\n    const wrapper = document.createElement(&#39;div&#39;);\n    wrapper.setAttribute(&#39;class&#39;, &#39;hello-wrapper&#39;);\n\n    this.greeting = document.createElement(&#39;h3&#39;);\n    this.greeting.setAttribute(&#39;class&#39;, &#39;greeting&#39;);\n\n    // Apply styles\n    const style = document.createElement(&#39;style&#39;);\n    style.textContent = `\n      .hello-wrapper {\n        padding: 20px;\n        background: linear-gradient(135deg, #6e8efb, #a777e3);\n        border-radius: 8px;\n        box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n        color: white;\n        text-align: center;\n        transition: transform 0.3s ease;\n      }\n      \n      .hello-wrapper:hover {\n        transform: translateY(-5px);\n      }\n      \n      .greeting {\n        margin: 0;\n        font-weight: 500;\n      }\n    `;\n\n    // Attach elements to shadow DOM\n    shadow.appendChild(style);\n    shadow.appendChild(wrapper);\n    wrapper.appendChild(this.greeting);\n\n    // Log constructor call\n    this.logLifecycleEvent(&#39;constructor&#39;, &#39;Component created&#39;);\n  }\n\n  // Called when element is inserted into the DOM\n  connectedCallback() {\n    // Update the greeting text\n    this.updateGreeting();\n    this.logLifecycleEvent(&#39;connected&#39;, &#39;Component added to DOM&#39;);\n  }\n\n  // Called when element is removed from the DOM\n  disconnectedCallback() {\n    this.logLifecycleEvent(&#39;disconnected&#39;, &#39;Component removed from DOM&#39;);\n  }\n\n  // Called when one of the observed attributes changes\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (oldValue !== newValue) {\n      this.logLifecycleEvent(\n      &#39;attribute-changed&#39;,\n      `Attribute &quot;${name}&quot; changed from &quot;${oldValue || &#39;none&#39;}&quot; to &quot;${newValue}&quot;`);\n\n\n      if (name === &#39;name&#39;) {\n        this.updateGreeting();\n      }\n    }\n  }\n\n  // Helper method to update the greeting text\n  updateGreeting() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;World&#39;;\n    this.greeting.textContent = `Hello, ${name}!`;\n  }\n\n  // Helper method to log lifecycle events\n  logLifecycleEvent(type, message) {\n    const logContainer = document.getElementById(&#39;lifecycle-events&#39;);\n    if (logContainer) {\n      const entry = document.createElement(&#39;div&#39;);\n      entry.className = `log-entry ${type}`;\n      entry.textContent = `${new Date().toLocaleTimeString()} - ${message}`;\n      logContainer.appendChild(entry);\n      logContainer.scrollTop = logContainer.scrollHeight;\n    }\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;hello-world&#39;, HelloWorld);\n\n// Setup UI interaction\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  const container = document.getElementById(&#39;component-container&#39;);\n  const nameInput = document.getElementById(&#39;name-input&#39;);\n  const updateBtn = document.getElementById(&#39;update-btn&#39;);\n  const addBtn = document.getElementById(&#39;add-btn&#39;);\n  const removeBtn = document.getElementById(&#39;remove-btn&#39;);\n  const clearLogBtn = document.getElementById(&#39;clear-log&#39;);\n  const logContainer = document.getElementById(&#39;lifecycle-events&#39;);\n\n  let component = null;\n\n  // Add component to DOM\n  addBtn.addEventListener(&#39;click&#39;, () =&gt; {\n    if (!container.contains(component)) {\n      component = document.createElement(&#39;hello-world&#39;);\n      component.setAttribute(&#39;name&#39;, nameInput.value);\n      container.appendChild(component);\n    }\n  });\n\n  // Remove component from DOM\n  removeBtn.addEventListener(&#39;click&#39;, () =&gt; {\n    if (component &amp;&amp; container.contains(component)) {\n      container.removeChild(component);\n      component = null;\n    }\n  });\n\n  // Update name attribute\n  updateBtn.addEventListener(&#39;click&#39;, () =&gt; {\n    if (component) {\n      component.setAttribute(&#39;name&#39;, nameInput.value);\n    }\n  });\n\n  // Clear log\n  clearLogBtn.addEventListener(&#39;click&#39;, () =&gt; {\n    logContainer.innerHTML = &#39;&#39;;\n  });\n\n  // Initial log message\n  const entry = document.createElement(&#39;div&#39;);\n  entry.className = &#39;log-entry&#39;;\n  entry.textContent = `${new Date().toLocaleTimeString()} - Demo initialized. Click &quot;Add to DOM&quot; to create the component.`;\n  logContainer.appendChild(entry);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>Try clicking the buttons to see the lifecycle methods in action. This simple example demonstrates the core concepts of Web Components: custom elements, shadow DOM encapsulation, and lifecycle callbacks.</p>\n<h2 id=\"who-s-using-web-components-today\">Who's Using Web Components Today?<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#who-s-using-web-components-today\" aria-label=\"Link to this section\">#</a></h2>\n<p>The short answer is at the top of this post. Here is what each name actually offers, limited to things you can install and read:</p>\n<ul>\n<li><strong>Google</strong>: maintains Lit, and Material Web (<code>@material/web</code>), Google's Material Design components as web components.</li>\n<li><strong>Microsoft</strong>: publishes the FAST library and Fluent UI web components.</li>\n<li><strong>Adobe</strong>: Spectrum Web Components, the web implementation of its Spectrum design system.</li>\n<li><strong>IBM</strong>: the Carbon design system has a web components package.</li>\n<li><strong>Salesforce</strong>: Lightning Web Components is a framework built on the web components standards.</li>\n<li><strong>GitHub</strong>: has published small custom elements as open source (for example <code>&lt;relative-time&gt;</code>).</li>\n<li><strong>ING</strong>: publishes the open-source Lion web components library.</li>\n</ul>\n<p>Claims that a particular big site (a video player, a map, an editor, a streaming service) is \"built with web components\" are common online. They are not backed by public sources, so this post leaves them out. Check a site's own engineering posts before repeating one.</p>\n<h2 id=\"popular-web-component-libraries-hosting-strategies\">Popular Web Component Libraries &amp; Hosting Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#popular-web-component-libraries-hosting-strategies\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"libraries-and-frameworks\">Libraries and Frameworks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#libraries-and-frameworks\" aria-label=\"Link to this section\">#</a></h3>\n<p>Several libraries have emerged to simplify Web Component development:</p>\n<ol>\n<li>\n<p><strong>Lit</strong>: Google's lightweight library (successor to Polymer) for creating Web Components with minimal boilerplate.</p>\n</li>\n<li>\n<p><strong>Stencil</strong>: Created by the Ionic team, Stencil generates highly optimized Web Components and can output framework-specific wrappers.</p>\n</li>\n<li>\n<p><strong>FAST</strong>: Microsoft's adaptive UI library produces Web Components that automatically adapt to user preferences.</p>\n</li>\n<li>\n<p><strong>Shoelace</strong>: A collection of professionally designed, every day UI components built on a framework-agnostic technology.</p>\n</li>\n<li>\n<p><strong>Enhance</strong>: A framework that uses progressive enhancement to create multi-page applications with Web Components.</p>\n</li>\n</ol>\n<h3 id=\"hosting-distribution-strategies\">Hosting &amp; Distribution Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#hosting-distribution-strategies\" aria-label=\"Link to this section\">#</a></h3>\n<p>Companies distribute their Web Components in several ways:</p>\n<ol>\n<li>\n<p><strong>NPM Packages</strong>: The most common approach, allowing developers to install components via package managers.</p>\n<ul>\n<li>Example: <code>npm install @adobe/spectrum-web-components</code></li>\n</ul>\n</li>\n<li>\n<p><strong>CDN Delivery</strong>: Pre-built components hosted on CDNs allow for quick inclusion without build steps.</p>\n<ul>\n<li>Example: <code>&lt;script type=\"module\" src=\"https://unpkg.com/@shoelace-style/shoelace@2.0.0/dist/shoelace.js\"&gt;&lt;/script&gt;</code></li>\n</ul>\n</li>\n<li>\n<p><strong>Custom Element Registries</strong>: Specialized registries like <a href=\"https://www.webcomponents.org/\">webcomponents.org</a> catalog available components.</p>\n</li>\n<li>\n<p><strong>Bundled with Applications</strong>: Many applications bundle custom elements as part of their build process.</p>\n</li>\n<li>\n<p><strong>Micro-frontend Architecture</strong>: Components distributed across multiple teams but united in a single application.</p>\n</li>\n</ol>\n<h2 id=\"common-patterns-in-production-web-components\">Common Patterns in Production Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#common-patterns-in-production-web-components\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"composition-pattern\">Composition Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#composition-pattern\" aria-label=\"Link to this section\">#</a></h3>\n<p>One of the most powerful patterns is component composition:</p>\n<pre><code class=\"language-html\">&lt;!-- Container component --&gt;\n&lt;user-profile&gt;\n  &lt;!-- Composable child components --&gt;\n  &lt;user-avatar slot=\"avatar\"&gt;&lt;/user-avatar&gt;\n  &lt;user-info slot=\"info\"&gt;&lt;/user-info&gt;\n  &lt;user-stats slot=\"stats\"&gt;&lt;/user-stats&gt;\n&lt;/user-profile&gt;\n</code></pre>\n<p>This pattern enables teams to build complex UIs from simpler building blocks, a strategy employed by many design systems.</p>\n<p>To see the composition pattern in action, here's an interactive demo that demonstrates how slots distribute content in Web Components:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Web Component Composition Pattern Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Web Component Composition Pattern Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;explanation&quot;&gt;\n    &lt;p&gt;This demo implements the &lt;code&gt;&amp;lt;user-profile&amp;gt;&lt;/code&gt; composition pattern shown in the example above.&lt;/p&gt;\n    &lt;p&gt;Toggle sections to see how content is distributed into different slots within the component.&lt;/p&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;controls&quot;&gt;\n    &lt;button id=&quot;toggle-avatar&quot; class=&quot;active&quot;&gt;Toggle Avatar&lt;/button&gt;\n    &lt;button id=&quot;toggle-info&quot; class=&quot;active&quot;&gt;Toggle Info&lt;/button&gt;\n    &lt;button id=&quot;toggle-stats&quot; class=&quot;active&quot;&gt;Toggle Stats&lt;/button&gt;\n    &lt;button id=&quot;toggle-theme&quot;&gt;Change Theme&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;demo-container&quot;&gt;\n    &lt;div class=&quot;component-view&quot;&gt;\n      &lt;h3&gt;Component Preview&lt;/h3&gt;\n      &lt;div class=&quot;profile-container&quot;&gt;\n        &lt;!-- The actual user-profile component --&gt;\n        &lt;user-profile theme=&quot;light&quot;&gt;\n          &lt;!-- Avatar slot content --&gt;\n          &lt;div slot=&quot;avatar&quot; id=&quot;avatar-content&quot;&gt;\n            &lt;img src=&quot;https://randomuser.me/api/portraits/women/42.jpg&quot; alt=&quot;User avatar&quot;&gt;\n          &lt;/div&gt;\n          \n          &lt;!-- Info slot content --&gt;\n          &lt;div slot=&quot;info&quot; id=&quot;info-content&quot;&gt;\n            &lt;h3&gt;Sarah Johnson&lt;/h3&gt;\n            &lt;p class=&quot;job-title&quot;&gt;Senior Web Developer&lt;/p&gt;\n            &lt;p class=&quot;location&quot;&gt;San Francisco, CA&lt;/p&gt;\n          &lt;/div&gt;\n          \n          &lt;!-- Stats slot content --&gt;\n          &lt;div slot=&quot;stats&quot; id=&quot;stats-content&quot;&gt;\n            &lt;div class=&quot;stat-item&quot;&gt;\n              &lt;span class=&quot;stat-value&quot;&gt;142&lt;/span&gt;\n              &lt;span class=&quot;stat-label&quot;&gt;Projects&lt;/span&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;stat-item&quot;&gt;\n              &lt;span class=&quot;stat-value&quot;&gt;3.2K&lt;/span&gt;\n              &lt;span class=&quot;stat-label&quot;&gt;Followers&lt;/span&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;stat-item&quot;&gt;\n              &lt;span class=&quot;stat-value&quot;&gt;87&lt;/span&gt;\n              &lt;span class=&quot;stat-label&quot;&gt;Reviews&lt;/span&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/user-profile&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;structure-diagram&quot;&gt;\n      &lt;h3&gt;Component Structure&lt;/h3&gt;\n      &lt;div class=&quot;diagram&quot;&gt;\n        &lt;div class=&quot;shadow-dom&quot;&gt;\n          &lt;div class=&quot;shadow-label&quot;&gt;Shadow DOM&lt;/div&gt;\n          &lt;div class=&quot;slot-container&quot;&gt;\n            &lt;div class=&quot;slot avatar-slot&quot;&gt;\n              &lt;span class=&quot;slot-label&quot;&gt;slot=&quot;avatar&quot;&lt;/span&gt;\n              &lt;div class=&quot;slot-content&quot; id=&quot;avatar-indicator&quot;&gt;Content from Light DOM&lt;/div&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;slot info-slot&quot;&gt;\n              &lt;span class=&quot;slot-label&quot;&gt;slot=&quot;info&quot;&lt;/span&gt;\n              &lt;div class=&quot;slot-content&quot; id=&quot;info-indicator&quot;&gt;Content from Light DOM&lt;/div&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;slot stats-slot&quot;&gt;\n              &lt;span class=&quot;slot-label&quot;&gt;slot=&quot;stats&quot;&lt;/span&gt;\n              &lt;div class=&quot;slot-content&quot; id=&quot;stats-indicator&quot;&gt;Content from Light DOM&lt;/div&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;code-preview&quot;&gt;\n    &lt;h3&gt;HTML Code&lt;/h3&gt;\n    &lt;pre&gt;&lt;code id=&quot;code-output&quot;&gt;&lt;/code&gt;&lt;/pre&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  --primary-color: #4a6cf7;\n  --primary-light: #e0e7ff;\n  --avatar-slot-color: #ec4899;\n  --info-slot-color: #10b981;\n  --stats-slot-color: #f59e0b;\n  --text-dark: #1f2937;\n  --text-light: #f9fafb;\n  --bg-light: #ffffff;\n  --bg-dark: #1f2937;\n  --border-light: #e5e7eb;\n  --border-dark: #4b5563;\n  --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n  \n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nbody {\n  font-family: var(--font-family);\n  color: var(--text-dark);\n  background-color: #f3f4f6;\n  line-height: 1.5;\n  padding: 20px;\n}\n\n.container {\n  max-width: 900px;\n  margin: 0 auto;\n}\n\nh2 {\n  color: var(--primary-color);\n  margin-bottom: 24px;\n  text-align: center;\n}\n\nh3 {\n  margin-bottom: 16px;\n  font-weight: 600;\n}\n\n.explanation {\n  background-color: var(--primary-light);\n  border-left: 4px solid var(--primary-color);\n  padding: 16px;\n  margin-bottom: 24px;\n  border-radius: 0 8px 8px 0;\n}\n\n.controls {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-bottom: 24px;\n}\n\nbutton {\n  padding: 8px 16px;\n  background-color: #f3f4f6;\n  border: 1px solid #d1d5db;\n  border-radius: 6px;\n  font-family: inherit;\n  font-size: 14px;\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\nbutton:hover {\n  background-color: #e5e7eb;\n}\n\nbutton.active {\n  background-color: var(--primary-color);\n  color: white;\n  border-color: var(--primary-color);\n}\n\n.demo-container {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 24px;\n  margin-bottom: 24px;\n}\n\n@media (max-width: 768px) {\n  .demo-container {\n    grid-template-columns: 1fr;\n  }\n}\n\n.component-view, .structure-diagram {\n  background-color: white;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: var(--shadow);\n}\n\n.profile-container {\n  border: 2px dashed #d1d5db;\n  padding: 20px;\n  border-radius: 8px;\n  display: flex;\n  justify-content: center;\n}\n\n/* Component styling */\nuser-profile {\n  display: block;\n  width: 100%;\n  max-width: 400px;\n  transition: all 0.3s ease;\n}\n\n/* Light DOM content styling */\n[slot=&quot;avatar&quot;] img {\n  width: 100px;\n  height: 100px;\n  border-radius: 50%;\n  object-fit: cover;\n}\n\n[slot=&quot;info&quot;] h3 {\n  margin: 0 0 8px 0;\n  font-size: 20px;\n}\n\n.job-title {\n  color: #4b5563;\n  margin-bottom: 4px;\n}\n\n.location {\n  color: #6b7280;\n  font-size: 14px;\n}\n\n.stat-item {\n  text-align: center;\n}\n\n.stat-value {\n  display: block;\n  font-size: 20px;\n  font-weight: 600;\n  color: var(--primary-color);\n}\n\n.stat-label {\n  font-size: 14px;\n  color: #6b7280;\n}\n\n/* Structure diagram styling */\n.diagram {\n  border: 2px dashed #d1d5db;\n  padding: 20px;\n  border-radius: 8px;\n}\n\n.shadow-dom {\n  border: 2px solid var(--primary-color);\n  border-radius: 8px;\n  padding: 20px;\n  position: relative;\n}\n\n.shadow-label {\n  position: absolute;\n  top: -12px;\n  left: 16px;\n  background-color: var(--primary-color);\n  color: white;\n  padding: 2px 8px;\n  border-radius: 4px;\n  font-size: 14px;\n}\n\n.slot-container {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  margin-top: 12px;\n}\n\n.slot {\n  border: 2px solid;\n  border-radius: 6px;\n  padding: 12px;\n  position: relative;\n  transition: all 0.3s ease;\n}\n\n.slot.empty {\n  opacity: 0.6;\n  background-color: #f9fafb;\n}\n\n.avatar-slot {\n  border-color: var(--avatar-slot-color);\n}\n\n.info-slot {\n  border-color: var(--info-slot-color);\n}\n\n.stats-slot {\n  border-color: var(--stats-slot-color);\n}\n\n.slot-label {\n  position: absolute;\n  top: -10px;\n  left: 10px;\n  padding: 0 6px;\n  border-radius: 4px;\n  font-size: 12px;\n  font-weight: 500;\n  color: white;\n}\n\n.avatar-slot .slot-label {\n  background-color: var(--avatar-slot-color);\n}\n\n.info-slot .slot-label {\n  background-color: var(--info-slot-color);\n}\n\n.stats-slot .slot-label {\n  background-color: var(--stats-slot-color);\n}\n\n.slot-content {\n  font-family: monospace;\n  font-size: 14px;\n  color: #6b7280;\n}\n\n/* Code preview styling */\n.code-preview {\n  background-color: white;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: var(--shadow);\n}\n\npre {\n  background-color: #1e293b;\n  color: #e2e8f0;\n  padding: 16px;\n  border-radius: 6px;\n  overflow-x: auto;\n  font-family: monospace;\n  font-size: 14px;\n  line-height: 1.5;\n}\n\n/* Animation for slot content highlight */\n@keyframes highlight {\n  0% { background-color: rgba(255, 255, 100, 0.3); }\n  100% { background-color: transparent; }\n}\n\n.highlight {\n  animation: highlight 1s ease;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the UserProfile Component\nclass UserProfile extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initialize properties\n    this._theme = this.getAttribute(&#39;theme&#39;) || &#39;light&#39;;\n\n    // Render the component\n    this.render();\n  }\n\n  // Observed attributes\n  static get observedAttributes() {\n    return [&#39;theme&#39;];\n  }\n\n  // Handle attribute changes\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;theme&#39; &amp;&amp; oldValue !== newValue) {\n      this._theme = newValue;\n      this.render();\n    }\n  }\n\n  // Render the component\n  render() {\n    const isDark = this._theme === &#39;dark&#39;;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .profile {\n          background-color: ${isDark ? &#39;var(--bg-dark, #1f2937)&#39; : &#39;var(--bg-light, #ffffff)&#39;};\n          color: ${isDark ? &#39;var(--text-light, #f9fafb)&#39; : &#39;var(--text-dark, #1f2937)&#39;};\n          border-radius: 12px;\n          overflow: hidden;\n          box-shadow: var(--shadow, 0 4px 6px -1px rgba(0, 0, 0, 0.1));\n          transition: all 0.3s ease;\n        }\n        \n        .profile-header {\n          display: flex;\n          align-items: center;\n          padding: 24px;\n          background-color: ${isDark ? &#39;rgba(255, 255, 255, 0.05)&#39; : &#39;rgba(0, 0, 0, 0.02)&#39;};\n          border-bottom: 1px solid ${isDark ? &#39;var(--border-dark, #4b5563)&#39; : &#39;var(--border-light, #e5e7eb)&#39;};\n        }\n        \n        .avatar-container {\n          margin-right: 20px;\n        }\n        \n        .info-container {\n          flex: 1;\n        }\n        \n        .stats-container {\n          display: flex;\n          justify-content: space-between;\n          padding: 16px 24px;\n          border-top: 1px solid ${isDark ? &#39;var(--border-dark, #4b5563)&#39; : &#39;var(--border-light, #e5e7eb)&#39;};\n        }\n        \n        /* Empty slot styling */\n        .avatar-container:empty::before,\n        .info-container:empty::before,\n        .stats-container:empty::before {\n          display: block;\n          padding: 16px;\n          background-color: ${isDark ? &#39;rgba(255, 255, 255, 0.05)&#39; : &#39;rgba(0, 0, 0, 0.03)&#39;};\n          border-radius: 6px;\n          font-style: italic;\n          font-size: 14px;\n          color: ${isDark ? &#39;rgba(255, 255, 255, 0.5)&#39; : &#39;rgba(0, 0, 0, 0.4)&#39;};\n        }\n        \n        .avatar-container:empty::before {\n          content: &#39;No avatar content provided&#39;;\n          width: 80px;\n          height: 80px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n          text-align: center;\n          border-radius: 50%;\n        }\n        \n        .info-container:empty::before {\n          content: &#39;No user info provided&#39;;\n        }\n        \n        .stats-container:empty::before {\n          content: &#39;No stats content provided&#39;;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;profile&quot;&gt;\n        &lt;div class=&quot;profile-header&quot;&gt;\n          &lt;div class=&quot;avatar-container&quot;&gt;\n            &lt;slot name=&quot;avatar&quot;&gt;&lt;/slot&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;info-container&quot;&gt;\n            &lt;slot name=&quot;info&quot;&gt;&lt;/slot&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;stats-container&quot;&gt;\n          &lt;slot name=&quot;stats&quot;&gt;&lt;/slot&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Set up slot change listeners\n    this.shadowRoot.querySelectorAll(&#39;slot&#39;).forEach(slot =&gt; {\n      slot.addEventListener(&#39;slotchange&#39;, () =&gt; this.handleSlotChange(slot));\n    });\n\n    // Update code preview\n    updateCodePreview();\n  }\n\n  // Handle slot content changes\n  handleSlotChange(slot) {\n    const slotName = slot.name;\n    const hasContent = slot.assignedNodes().length &gt; 0;\n\n    // Update the diagram to reflect slot status\n    const indicator = document.getElementById(`${slotName}-indicator`);\n    if (indicator) {\n      const slotElement = indicator.closest(&#39;.slot&#39;);\n\n      if (hasContent) {\n        slotElement.classList.remove(&#39;empty&#39;);\n        indicator.textContent = &#39;Content from Light DOM&#39;;\n        indicator.classList.add(&#39;highlight&#39;);\n        setTimeout(() =&gt; indicator.classList.remove(&#39;highlight&#39;), 1000);\n      } else {\n        slotElement.classList.add(&#39;empty&#39;);\n        indicator.textContent = &#39;Empty slot (no content provided)&#39;;\n      }\n    }\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;user-profile&#39;, UserProfile);\n\n// Function to update code preview\nfunction updateCodePreview() {\n  const avatarVisible = document.getElementById(&#39;avatar-content&#39;).style.display !== &#39;none&#39;;\n  const infoVisible = document.getElementById(&#39;info-content&#39;).style.display !== &#39;none&#39;;\n  const statsVisible = document.getElementById(&#39;stats-content&#39;).style.display !== &#39;none&#39;;\n  const profileElement = document.querySelector(&#39;user-profile&#39;);\n  const theme = profileElement.getAttribute(&#39;theme&#39;) || &#39;light&#39;;\n\n  let code = `&lt;!-- Container component --&gt;\\n&lt;user-profile theme=&quot;${theme}&quot;&gt;\\n`;\n\n  // Add avatar section if visible\n  if (avatarVisible) {\n    code += `  &lt;!-- Composable child component for avatar --&gt;\\n`;\n    code += `  &lt;div slot=&quot;avatar&quot;&gt;\\n`;\n    code += `    &lt;img src=&quot;https://randomuser.me/api/portraits/women/42.jpg&quot; alt=&quot;User avatar&quot;&gt;\\n`;\n    code += `  &lt;/div&gt;\\n\\n`;\n  }\n\n  // Add info section if visible\n  if (infoVisible) {\n    code += `  &lt;!-- Composable child component for user info --&gt;\\n`;\n    code += `  &lt;div slot=&quot;info&quot;&gt;\\n`;\n    code += `    &lt;h3&gt;Sarah Johnson&lt;/h3&gt;\\n`;\n    code += `    &lt;p class=&quot;job-title&quot;&gt;Senior Web Developer&lt;/p&gt;\\n`;\n    code += `    &lt;p class=&quot;location&quot;&gt;San Francisco, CA&lt;/p&gt;\\n`;\n    code += `  &lt;/div&gt;\\n\\n`;\n  }\n\n  // Add stats section if visible\n  if (statsVisible) {\n    code += `  &lt;!-- Composable child component for stats --&gt;\\n`;\n    code += `  &lt;div slot=&quot;stats&quot;&gt;\\n`;\n    code += `    &lt;div class=&quot;stat-item&quot;&gt;\\n`;\n    code += `      &lt;span class=&quot;stat-value&quot;&gt;142&lt;/span&gt;\\n`;\n    code += `      &lt;span class=&quot;stat-label&quot;&gt;Projects&lt;/span&gt;\\n`;\n    code += `    &lt;/div&gt;\\n`;\n    code += `    &lt;div class=&quot;stat-item&quot;&gt;\\n`;\n    code += `      &lt;span class=&quot;stat-value&quot;&gt;3.2K&lt;/span&gt;\\n`;\n    code += `      &lt;span class=&quot;stat-label&quot;&gt;Followers&lt;/span&gt;\\n`;\n    code += `    &lt;/div&gt;\\n`;\n    code += `    &lt;div class=&quot;stat-item&quot;&gt;\\n`;\n    code += `      &lt;span class=&quot;stat-value&quot;&gt;87&lt;/span&gt;\\n`;\n    code += `      &lt;span class=&quot;stat-label&quot;&gt;Reviews&lt;/span&gt;\\n`;\n    code += `    &lt;/div&gt;\\n`;\n    code += `  &lt;/div&gt;\\n`;\n  }\n\n  code += `&lt;/user-profile&gt;`;\n\n  // Update the code display\n  document.getElementById(&#39;code-output&#39;).textContent = code;\n}\n\n// Set up the UI controls\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  const avatarToggle = document.getElementById(&#39;toggle-avatar&#39;);\n  const infoToggle = document.getElementById(&#39;toggle-info&#39;);\n  const statsToggle = document.getElementById(&#39;toggle-stats&#39;);\n  const themeToggle = document.getElementById(&#39;toggle-theme&#39;);\n\n  const avatarContent = document.getElementById(&#39;avatar-content&#39;);\n  const infoContent = document.getElementById(&#39;info-content&#39;);\n  const statsContent = document.getElementById(&#39;stats-content&#39;);\n  const profileElement = document.querySelector(&#39;user-profile&#39;);\n\n  // Toggle avatar content\n  avatarToggle.addEventListener(&#39;click&#39;, () =&gt; {\n    avatarToggle.classList.toggle(&#39;active&#39;);\n    if (avatarToggle.classList.contains(&#39;active&#39;)) {\n      avatarContent.style.display = &#39;&#39;;\n    } else {\n      avatarContent.style.display = &#39;none&#39;;\n    }\n    updateCodePreview();\n  });\n\n  // Toggle info content\n  infoToggle.addEventListener(&#39;click&#39;, () =&gt; {\n    infoToggle.classList.toggle(&#39;active&#39;);\n    if (infoToggle.classList.contains(&#39;active&#39;)) {\n      infoContent.style.display = &#39;&#39;;\n    } else {\n      infoContent.style.display = &#39;none&#39;;\n    }\n    updateCodePreview();\n  });\n\n  // Toggle stats content\n  statsToggle.addEventListener(&#39;click&#39;, () =&gt; {\n    statsToggle.classList.toggle(&#39;active&#39;);\n    if (statsToggle.classList.contains(&#39;active&#39;)) {\n      statsContent.style.display = &#39;&#39;;\n    } else {\n      statsContent.style.display = &#39;none&#39;;\n    }\n    updateCodePreview();\n  });\n\n  // Toggle theme\n  themeToggle.addEventListener(&#39;click&#39;, () =&gt; {\n    themeToggle.classList.toggle(&#39;active&#39;);\n    const newTheme = profileElement.getAttribute(&#39;theme&#39;) === &#39;light&#39; ? &#39;dark&#39; : &#39;light&#39;;\n    profileElement.setAttribute(&#39;theme&#39;, newTheme);\n    if (newTheme === &#39;dark&#39;) {\n      themeToggle.textContent = &#39;Switch to Light&#39;;\n    } else {\n      themeToggle.textContent = &#39;Switch to Dark&#39;;\n    }\n    updateCodePreview();\n  });\n\n  // Initialize the component\n  setTimeout(() =&gt; {\n    // Trigger slot change events\n    profileElement.shadowRoot.querySelectorAll(&#39;slot&#39;).forEach(slot =&gt; {\n      const event = new Event(&#39;slotchange&#39;);\n      slot.dispatchEvent(event);\n    });\n\n    // Initialize code preview\n    updateCodePreview();\n  }, 100);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This card component uses three distinct slots (header, body, and footer) to create a flexible, reusable UI element. Try toggling different parts of the content to see how:</p>\n<ol>\n<li>\n<p>The component maintains its structure even when slots are empty</p>\n</li>\n<li>\n<p>Content from the \"light DOM\" (your HTML) flows into the designated slots</p>\n</li>\n<li>\n<p>The component's styling remains consistent regardless of the content provided</p>\n</li>\n<li>\n<p>The theme can change while preserving the composition structure</p>\n</li>\n<li>\n<p>The code preview updates to show exactly what HTML you would write to achieve the current state</p>\n</li>\n</ol>\n<p>This pattern is especially powerful for design systems, as it allows components to maintain consistent styling and behavior while giving content authors flexibility to provide custom content for different sections.</p>\n<h3 id=\"property-attribute-pattern\">Property/Attribute Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#property-attribute-pattern\" aria-label=\"Link to this section\">#</a></h3>\n<p>Attributes for simple values, properties for complex data:</p>\n<pre><code class=\"language-javascript\">class DataTable extends HTMLElement {\n  // Simple configuration via attributes\n  static get observedAttributes() { \n    return ['loading', 'pagination']; \n  }\n\n  // Complex data via properties\n  set data(value) {\n    this._data = value;\n    this._renderTable();\n  }\n}\n</code></pre>\n<p>An <strong>attribute</strong> is the text you write in HTML (<code>&lt;my-el size=\"large\"&gt;</code>), and it is always a string. A <strong>property</strong> is a field on the element object in JavaScript (<code>el.items = [1, 2, 3]</code>), and it can hold anything: arrays, objects, functions. Use attributes for small settings you want visible in markup, and properties for real data.</p>\n<p>The failure first: ask what an attribute does to an array.</p>\n<pre><code class=\"language-js\">el.setAttribute('items', ['a', 'b']);\nel.getAttribute('items'); // \"a,b\"  (one string, not an array)\nel.items = ['a', 'b'];     // the array itself, if the component has an items property\n</code></pre>\n<p>To better understand how attributes and properties work together in Web Components, let's explore this interactive demo that clearly demonstrates their differences:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Property/Attribute Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Property vs. Attribute Pattern Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;explanation&quot;&gt;\n    &lt;p&gt;&lt;strong&gt;Attributes&lt;/strong&gt; are for simple values (strings, booleans) and appear in HTML markup. They can be observed with &lt;code&gt;attributeChangedCallback()&lt;/code&gt;.&lt;/p&gt;\n    &lt;p&gt;&lt;strong&gt;Properties&lt;/strong&gt; are for complex data (objects, arrays) and are set programmatically. They&#39;re not reflected in the HTML markup.&lt;/p&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;demo-section&quot;&gt;\n    &lt;div class=&quot;controls-section&quot;&gt;\n      &lt;div class=&quot;control-group&quot;&gt;\n        &lt;h3&gt;Attributes (Simple Values)&lt;/h3&gt;\n        &lt;div class=&quot;control&quot;&gt;\n          &lt;label&gt;Theme:&lt;/label&gt;\n          &lt;select id=&quot;theme-select&quot;&gt;\n            &lt;option value=&quot;light&quot;&gt;Light&lt;/option&gt;\n            &lt;option value=&quot;dark&quot;&gt;Dark&lt;/option&gt;\n            &lt;option value=&quot;colorful&quot;&gt;Colorful&lt;/option&gt;\n          &lt;/select&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control&quot;&gt;\n          &lt;label&gt;Size:&lt;/label&gt;\n          &lt;select id=&quot;size-select&quot;&gt;\n            &lt;option value=&quot;small&quot;&gt;Small&lt;/option&gt;\n            &lt;option value=&quot;medium&quot;&gt;Medium&lt;/option&gt;\n            &lt;option value=&quot;large&quot;&gt;Large&lt;/option&gt;\n          &lt;/select&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control&quot;&gt;\n          &lt;label&gt;Disabled:&lt;/label&gt;\n          &lt;input type=&quot;checkbox&quot; id=&quot;disabled-toggle&quot;&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control-group&quot;&gt;\n        &lt;h3&gt;Properties (Complex Data)&lt;/h3&gt;\n        &lt;div class=&quot;control&quot;&gt;\n          &lt;label&gt;Set Items (Array):&lt;/label&gt;\n          &lt;select id=&quot;items-preset&quot;&gt;\n            &lt;option value=&quot;fruits&quot;&gt;Fruits&lt;/option&gt;\n            &lt;option value=&quot;colors&quot;&gt;Colors&lt;/option&gt;\n            &lt;option value=&quot;countries&quot;&gt;Countries&lt;/option&gt;\n          &lt;/select&gt;\n          &lt;button id=&quot;set-items&quot;&gt;Set Items Array&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;control&quot;&gt;\n          &lt;label&gt;Set Config (Object):&lt;/label&gt;\n          &lt;select id=&quot;config-preset&quot;&gt;\n            &lt;option value=&quot;default&quot;&gt;Default Config&lt;/option&gt;\n            &lt;option value=&quot;compact&quot;&gt;Compact Config&lt;/option&gt;\n            &lt;option value=&quot;expanded&quot;&gt;Expanded Config&lt;/option&gt;\n          &lt;/select&gt;\n          &lt;button id=&quot;set-config&quot;&gt;Set Config Object&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;component-container&quot;&gt;\n      &lt;h3&gt;Component Output&lt;/h3&gt;\n      &lt;data-display-component \n        theme=&quot;light&quot; \n        size=&quot;medium&quot;&gt;\n      &lt;/data-display-component&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;state-display&quot;&gt;\n    &lt;div class=&quot;state-section&quot;&gt;\n      &lt;h3&gt;Current HTML Markup&lt;/h3&gt;\n      &lt;pre id=&quot;html-display&quot;&gt;&lt;/pre&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;state-section&quot;&gt;\n      &lt;h3&gt;Current Properties&lt;/h3&gt;\n      &lt;pre id=&quot;props-display&quot;&gt;&lt;/pre&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;state-section&quot;&gt;\n      &lt;h3&gt;Component Events Log&lt;/h3&gt;\n      &lt;div id=&quot;events-log&quot;&gt;&lt;/div&gt;\n      &lt;button id=&quot;clear-log&quot;&gt;Clear Log&lt;/button&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.container {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n  max-width: 900px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n.explanation {\n  background-color: #f0f4f8;\n  padding: 15px;\n  border-radius: 6px;\n  margin-bottom: 20px;\n  border-left: 4px solid #4a6cf7;\n}\n\n.demo-section {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n  margin-bottom: 20px;\n}\n\n.controls-section {\n  flex: 1;\n  min-width: 300px;\n}\n\n.component-container {\n  flex: 1;\n  min-width: 300px;\n  padding: 15px;\n  border: 2px dashed #ccc;\n  border-radius: 6px;\n}\n\n.control-group {\n  background-color: #f8f9fa;\n  padding: 15px;\n  border-radius: 6px;\n  margin-bottom: 15px;\n}\n\n.control-group h3 {\n  margin-top: 0;\n  color: #4a6cf7;\n  font-weight: 500;\n}\n\n.control {\n  margin-bottom: 12px;\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.control label {\n  min-width: 120px;\n  font-weight: 500;\n}\n\nbutton {\n  padding: 8px 12px;\n  background-color: #4a6cf7;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 14px;\n}\n\nbutton:hover {\n  background-color: #3a5cf7;\n}\n\nselect, input {\n  padding: 8px;\n  border: 1px solid #ccc;\n  border-radius: 4px;\n}\n\n.state-display {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n  margin-top: 20px;\n}\n\n.state-section {\n  flex: 1;\n  min-width: 280px;\n  background-color: #f8f9fa;\n  padding: 15px;\n  border-radius: 6px;\n}\n\n.state-section h3 {\n  margin-top: 0;\n  color: #4a6cf7;\n  font-weight: 500;\n}\n\npre {\n  background-color: #f1f1f1;\n  padding: 10px;\n  border-radius: 4px;\n  overflow-x: auto;\n  font-family: monospace;\n  margin: 0;\n}\n\n#events-log {\n  height: 150px;\n  overflow-y: auto;\n  background-color: #f1f1f1;\n  padding: 10px;\n  border-radius: 4px;\n  font-family: monospace;\n  margin-bottom: 10px;\n}\n\n.log-entry {\n  margin-bottom: 5px;\n  padding-bottom: 5px;\n  border-bottom: 1px solid #ddd;\n}\n\n.log-attribute {\n  color: #e63946;\n}\n\n.log-property {\n  color: #2a9d8f;\n}\n\n#clear-log {\n  background-color: #6c757d;\n}\n\n/* Highlight the attributes vs. properties sections */\n.control-group:nth-child(1) {\n  border-left: 4px solid #e63946; /* Red for attributes */\n}\n\n.control-group:nth-child(2) {\n  border-left: 4px solid #2a9d8f; /* Green for properties */\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the custom component\nclass DataDisplayComponent extends HTMLElement {\n  constructor() {\n    super();\n\n    // Initialize private properties\n    this._items = [];\n    this._config = {\n      showHeaders: true,\n      maxItems: 5,\n      animation: &#39;fade&#39; };\n\n\n    // Create a shadow root\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Do initial render\n    this.render();\n\n    // Log the construction\n    this.logEvent(&#39;constructor&#39;, &#39;Component created&#39;);\n  }\n\n  // Specify which attributes should be observed\n  static get observedAttributes() {\n    return [&#39;theme&#39;, &#39;size&#39;, &#39;disabled&#39;];\n  }\n\n  // Lifecycle: when component is added to DOM\n  connectedCallback() {\n    this.logEvent(&#39;connected&#39;, &#39;Component added to DOM&#39;);\n    this.updateDisplay();\n  }\n\n  // Lifecycle: when observed attributes change\n  attributeChangedCallback(name, oldValue, newValue) {\n    this.logEvent(&#39;attribute-changed&#39;, `Attribute &#39;${name}&#39; changed from &#39;${oldValue}&#39; to &#39;${newValue}&#39;`);\n    this.render();\n  }\n\n  // PROPERTY for items (array)\n  get items() {\n    return this._items;\n  }\n\n  set items(newItems) {\n    this.logEvent(&#39;property-changed&#39;, &#39;Property &quot;items&quot; set with array data&#39;);\n    this._items = newItems;\n    this.render();\n  }\n\n  // PROPERTY for config (object)\n  get config() {\n    return this._config;\n  }\n\n  set config(newConfig) {\n    this.logEvent(&#39;property-changed&#39;, &#39;Property &quot;config&quot; set with object data&#39;);\n    this._config = { ...this._config, ...newConfig };\n    this.render();\n  }\n\n  // Helper for rendering the component\n  render() {\n    // Get attribute values\n    const theme = this.getAttribute(&#39;theme&#39;) || &#39;light&#39;;\n    const size = this.getAttribute(&#39;size&#39;) || &#39;medium&#39;;\n    const disabled = this.hasAttribute(&#39;disabled&#39;);\n\n    // Prepare styles based on attributes\n    const themeColors = {\n      light: { bg: &#39;#ffffff&#39;, text: &#39;#333333&#39;, border: &#39;#dddddd&#39; },\n      dark: { bg: &#39;#333333&#39;, text: &#39;#ffffff&#39;, border: &#39;#555555&#39; },\n      colorful: { bg: &#39;#f8f0ff&#39;, text: &#39;#4a0072&#39;, border: &#39;#9c27b0&#39; } };\n\n\n    const sizeValues = {\n      small: { padding: &#39;10px&#39;, fontSize: &#39;14px&#39; },\n      medium: { padding: &#39;15px&#39;, fontSize: &#39;16px&#39; },\n      large: { padding: &#39;20px&#39;, fontSize: &#39;18px&#39; } };\n\n\n    const colors = themeColors[theme] || themeColors.light;\n    const sizes = sizeValues[size] || sizeValues.medium;\n\n    // Generate the Shadow DOM content\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: inherit;\n        }\n        \n        .component {\n          background-color: ${colors.bg};\n          color: ${colors.text};\n          border: 1px solid ${colors.border};\n          border-radius: 8px;\n          padding: ${sizes.padding};\n          font-size: ${sizes.fontSize};\n          opacity: ${disabled ? &#39;0.6&#39; : &#39;1&#39;};\n          ${disabled ? &#39;pointer-events: none;&#39; : &#39;&#39;}\n          transition: all 0.3s ease;\n        }\n        \n        .info-section {\n          margin-bottom: 10px;\n        }\n        \n        .data-section {\n          border-top: 1px solid ${colors.border};\n          padding-top: 10px;\n          margin-top: 10px;\n        }\n        \n        ul {\n          margin: 0;\n          padding-left: 20px;\n        }\n        \n        li {\n          margin-bottom: 5px;\n        }\n        \n        .config-info {\n          font-family: monospace;\n          padding: 5px;\n          background: rgba(0,0,0,0.05);\n          border-radius: 4px;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;component&quot;&gt;\n        &lt;div class=&quot;info-section&quot;&gt;\n          &lt;strong&gt;Component States:&lt;/strong&gt;\n          &lt;div&gt;Theme: ${theme}&lt;/div&gt;\n          &lt;div&gt;Size: ${size}&lt;/div&gt;\n          &lt;div&gt;Disabled: ${disabled ? &#39;Yes&#39; : &#39;No&#39;}&lt;/div&gt;\n        &lt;/div&gt;\n        \n        ${this._items.length ? `\n          &lt;div class=&quot;data-section&quot;&gt;\n            ${this._config.showHeaders ? &#39;&lt;strong&gt;Items (from property):&lt;/strong&gt;&#39; : &#39;&#39;}\n            &lt;ul&gt;\n              ${this._items.slice(0, this._config.maxItems).map(item =&gt; `&lt;li&gt;${item}&lt;/li&gt;`).join(&#39;&#39;)}\n            &lt;/ul&gt;\n            ${this._items.length &gt; this._config.maxItems ? `&lt;small&gt;Showing ${this._config.maxItems} of ${this._items.length} items...&lt;/small&gt;` : &#39;&#39;}\n          &lt;/div&gt;\n        ` : &#39;&#39;}\n        \n        &lt;div class=&quot;data-section&quot;&gt;\n          &lt;strong&gt;Config (from property):&lt;/strong&gt;\n          &lt;div class=&quot;config-info&quot;&gt;\n            showHeaders: ${this._config.showHeaders}&lt;br&gt;\n            maxItems: ${this._config.maxItems}&lt;br&gt;\n            animation: ${this._config.animation}\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Update the state displays\n    this.updateDisplay();\n  }\n\n  // Helper for updating the display of HTML and properties\n  updateDisplay() {\n    const htmlDisplay = document.getElementById(&#39;html-display&#39;);\n    const propsDisplay = document.getElementById(&#39;props-display&#39;);\n\n    if (htmlDisplay) {\n      // Generate the current HTML representation\n      const html = `&lt;data-display-component\n  theme=&quot;${this.getAttribute(&#39;theme&#39;) || &#39;light&#39;}&quot;\n  size=&quot;${this.getAttribute(&#39;size&#39;) || &#39;medium&#39;}&quot;${this.hasAttribute(&#39;disabled&#39;) ? &#39;\\n  disabled&#39; : &#39;&#39;}&gt;\n&lt;/data-display-component&gt;`;\n\n      htmlDisplay.textContent = html;\n    }\n\n    if (propsDisplay) {\n      // Generate the current properties representation\n      const props = {\n        items: this._items,\n        config: this._config };\n\n\n      propsDisplay.textContent = JSON.stringify(props, null, 2);\n    }\n  }\n\n  // Helper for logging events\n  logEvent(type, message) {\n    const logContainer = document.getElementById(&#39;events-log&#39;);\n    if (logContainer) {\n      const entry = document.createElement(&#39;div&#39;);\n      entry.className = `log-entry log-${type.includes(&#39;attribute&#39;) ? &#39;attribute&#39; : &#39;property&#39;}`;\n      entry.textContent = `${new Date().toLocaleTimeString()} - ${message}`;\n      logContainer.appendChild(entry);\n      logContainer.scrollTop = logContainer.scrollHeight;\n    }\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;data-display-component&#39;, DataDisplayComponent);\n\n// Setup the demo controls\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  const component = document.querySelector(&#39;data-display-component&#39;);\n\n  // Attribute controls\n  document.getElementById(&#39;theme-select&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    component.setAttribute(&#39;theme&#39;, e.target.value);\n  });\n\n  document.getElementById(&#39;size-select&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    component.setAttribute(&#39;size&#39;, e.target.value);\n  });\n\n  document.getElementById(&#39;disabled-toggle&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    if (e.target.checked) {\n      component.setAttribute(&#39;disabled&#39;, &#39;&#39;);\n    } else {\n      component.removeAttribute(&#39;disabled&#39;);\n    }\n  });\n\n  // Property controls for items\n  document.getElementById(&#39;set-items&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n    const preset = document.getElementById(&#39;items-preset&#39;).value;\n    let items = [];\n\n    switch (preset) {\n      case &#39;fruits&#39;:\n        items = [&#39;Apple&#39;, &#39;Banana&#39;, &#39;Cherry&#39;, &#39;Dragon Fruit&#39;, &#39;Elderberry&#39;, &#39;Fig&#39;, &#39;Grapefruit&#39;];\n        break;\n      case &#39;colors&#39;:\n        items = [&#39;Red&#39;, &#39;Orange&#39;, &#39;Yellow&#39;, &#39;Green&#39;, &#39;Blue&#39;, &#39;Indigo&#39;, &#39;Violet&#39;];\n        break;\n      case &#39;countries&#39;:\n        items = [&#39;Australia&#39;, &#39;Brazil&#39;, &#39;Canada&#39;, &#39;Denmark&#39;, &#39;Egypt&#39;, &#39;France&#39;, &#39;Germany&#39;];\n        break;}\n\n\n    // Set the property\n    component.items = items;\n  });\n\n  // Property controls for config\n  document.getElementById(&#39;set-config&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n    const preset = document.getElementById(&#39;config-preset&#39;).value;\n    let config = {};\n\n    switch (preset) {\n      case &#39;default&#39;:\n        config = { showHeaders: true, maxItems: 5, animation: &#39;fade&#39; };\n        break;\n      case &#39;compact&#39;:\n        config = { showHeaders: false, maxItems: 3, animation: &#39;none&#39; };\n        break;\n      case &#39;expanded&#39;:\n        config = { showHeaders: true, maxItems: 10, animation: &#39;slide&#39; };\n        break;}\n\n\n    // Set the property\n    component.config = config;\n  });\n\n  // Clear log button\n  document.getElementById(&#39;clear-log&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n    document.getElementById(&#39;events-log&#39;).innerHTML = &#39;&#39;;\n  });\n\n  // Initialize displays\n  component.updateDisplay();\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This demo shows how attributes are best for simple values that can be represented in HTML markup (strings, booleans), while properties excel at handling complex data like arrays and objects. Try using the controls to manipulate both attributes and properties, and observe how:</p>\n<ol>\n<li>\n<p>Attribute changes trigger the <code>attributeChangedCallback()</code> and appear in the HTML markup</p>\n</li>\n<li>\n<p>Property changes don't affect the HTML markup but can handle complex data structures</p>\n</li>\n<li>\n<p>The component responds to both types of changes in different ways</p>\n</li>\n</ol>\n<p>This split is common in published libraries: small options such as a size or a disabled flag are attributes, and structured data is passed as a property.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You call <code>el.setAttribute('items', ['a', 'b'])</code>. What does <code>el.getAttribute('items')</code> return?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>The array <code>['a', 'b']</code></li>\n<li>The string <code>\"a,b\"</code></li>\n<li>An object with two entries</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The string <code>\"a,b\"</code></p>\n<p>Attributes only hold strings, so the array is converted with <code>toString()</code>. To hand the component a real array, set a property: <code>el.items = ['a', 'b']</code>.</p>\n</div>\n</div>\n</section>\n<p><strong>Coding challenge: Attribute or property?</strong></p>\n<p>Pass the same three fruits to <code>&lt;item-list&gt;</code> twice and compare what the component receives.</p>\n<p><strong>Expected result:</strong> after you finish step 1 the preview says the property is an array of 3 items; after step 2 the attribute line shows one string, <code>apple,pear,plum</code> (15 characters), and the property line is unchanged.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Pass an array: property vs attribute</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;item-list id=&quot;list&quot;&gt;&lt;/item-list&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class ItemList extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this._items = null;\n  }\n  set items(value) { this._items = value; this.render(); }\n  get items() { return this._items; }\n  static get observedAttributes() { return [&#39;items&#39;]; }\n  attributeChangedCallback() { this.render(); }\n  connectedCallback() { this.render(); }\n  render() {\n    const attr = this.getAttribute(&#39;items&#39;);\n    this.shadowRoot.innerHTML =\n      &#39;&lt;p&gt;property: &#39; + (Array.isArray(this._items) ? &#39;an array of &#39; + this._items.length : &#39;not set&#39;) + &#39;&lt;/p&gt;&#39; +\n      &#39;&lt;p&gt;attribute: &#39; + (attr === null ? &#39;not set&#39; : &#39;the string &quot;&#39; + attr + &#39;&quot; (&#39; + attr.length + &#39; characters)&#39;) + &#39;&lt;/p&gt;&#39;;\n  }\n}\ncustomElements.define(&#39;item-list&#39;, ItemList);\n\nconst el = document.getElementById(&#39;list&#39;);\n// TODO 1: set the property to [&#39;apple&#39;, &#39;pear&#39;, &#39;plum&#39;].\n// TODO 2: now set the attribute &#39;items&#39; to the same array with setAttribute.\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Step 1 is <code>el.items = [...]</code>. Step 2 is <code>el.setAttribute('items', [...])</code>. Notice the attribute line counts characters, not fruits.</p>\n</aside>\n<h3 id=\"event-based-communication-pattern\">Event-Based Communication Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#event-based-communication-pattern\" aria-label=\"Link to this section\">#</a></h3>\n<p>Components communicate through custom events:</p>\n<pre><code class=\"language-javascript\">// Inside the component\nthis.dispatchEvent(new CustomEvent('item-selected', {\n  bubbles: true,\n  composed: true, // Crosses shadow DOM boundary\n  detail: { id: selectedId }\n}));\n\n// Parent listening for events\ndocument.querySelector('my-component')\n  .addEventListener('item-selected', (e) =&gt; console.log(e.detail.id));\n</code></pre>\n<p>Many small custom elements in the wild, such as the open-source <code>&lt;relative-time&gt;</code> element, communicate this way.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A custom event is dispatched inside a component's shadow root. A normal (non-capturing) listener on <code>document</code> must receive it. Which options does the event need?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>bubbles: true</code> only</li>\n<li><code>composed: true</code> only</li>\n<li><code>bubbles: true</code> and <code>composed: true</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. <code>bubbles: true</code> and <code>composed: true</code></p>\n<p><code>bubbles</code> lets the event travel up through ancestors; <code>composed</code> lets it leave the shadow root. Without both, the event stops inside the shadow tree or never goes up.</p>\n</div>\n</div>\n</section>\n<p>Let's see events in action with this interactive demo. Here, two completely separate web components communicate through custom events without direct references to each other - a fundamental pattern for creating loosely coupled, reusable components:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Event Communication Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Web Component Event Communication Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;explanation&quot;&gt;\n    &lt;p&gt;This demo shows how Web Components can communicate using custom events. The color picker component dispatches events that the display component listens for.&lt;/p&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;component-wrapper&quot;&gt;\n    &lt;div class=&quot;component-section&quot;&gt;\n      &lt;h3&gt;Component 1: Color Picker&lt;/h3&gt;\n      &lt;color-picker-component&gt;&lt;/color-picker-component&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;event-flow&quot;&gt;\n      &lt;div class=&quot;event-arrow&quot;&gt;\n        &lt;div class=&quot;arrow-line&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;arrow-head&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;event-info&quot;&gt;\n        &lt;code&gt;color-changed&lt;/code&gt; event\n        &lt;div class=&quot;event-detail&quot;&gt;detail: { color, name }&lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;component-section&quot;&gt;\n      &lt;h3&gt;Component 2: Display&lt;/h3&gt;\n      &lt;color-display-component&gt;&lt;/color-display-component&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;event-log-section&quot;&gt;\n    &lt;h3&gt;Event Communication Log&lt;/h3&gt;\n    &lt;div id=&quot;event-log&quot;&gt;&lt;/div&gt;\n    &lt;button id=&quot;clear-log&quot;&gt;Clear Log&lt;/button&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">.container {\n  font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n  max-width: 800px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n.explanation {\n  background-color: #f0f8ff;\n  padding: 15px;\n  border-radius: 6px;\n  margin-bottom: 20px;\n  border-left: 4px solid #4a6cf7;\n}\n\n.component-wrapper {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin: 30px 0;\n  gap: 20px;\n}\n\n.component-section {\n  flex: 1;\n  min-width: 200px;\n  background-color: #f8f9fa;\n  padding: 20px;\n  border-radius: 8px;\n  box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n}\n\n.component-section h3 {\n  margin-top: 0;\n  color: #333;\n  font-size: 18px;\n  border-bottom: 1px solid #ddd;\n  padding-bottom: 10px;\n  margin-bottom: 15px;\n}\n\n.event-flow {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  padding: 0 10px;\n}\n\n.event-arrow {\n  position: relative;\n  width: 100%;\n  height: 40px;\n}\n\n.arrow-line {\n  position: absolute;\n  top: 50%;\n  left: 0;\n  right: 0;\n  height: 2px;\n  background-color: #4a6cf7;\n  transform: translateY(-50%);\n}\n\n.arrow-head {\n  position: absolute;\n  top: 50%;\n  right: 0;\n  width: 0;\n  height: 0;\n  border-top: 8px solid transparent;\n  border-bottom: 8px solid transparent;\n  border-left: 10px solid #4a6cf7;\n  transform: translateY(-50%);\n}\n\n.event-info {\n  background-color: #e8f0fe;\n  border: 1px solid #4a6cf7;\n  border-radius: 4px;\n  padding: 6px 10px;\n  font-size: 14px;\n  margin-top: 5px;\n  text-align: center;\n}\n\n.event-detail {\n  font-family: monospace;\n  font-size: 12px;\n  color: #666;\n  margin-top: 5px;\n}\n\n.event-log-section {\n  background-color: #f8f9fa;\n  padding: 20px;\n  border-radius: 8px;\n  margin-top: 30px;\n}\n\n.event-log-section h3 {\n  margin-top: 0;\n  color: #333;\n}\n\n#event-log {\n  background-color: #f1f1f1;\n  border: 1px solid #ddd;\n  border-radius: 4px;\n  padding: 10px;\n  height: 200px;\n  overflow-y: auto;\n  font-family: monospace;\n  margin-bottom: 10px;\n}\n\n.log-entry {\n  margin-bottom: 8px;\n  padding-bottom: 8px;\n  border-bottom: 1px solid #ddd;\n}\n\n.log-entry-time {\n  color: #666;\n  font-size: 12px;\n}\n\n.log-entry-message {\n  margin-top: 3px;\n}\n\n.log-highlight {\n  background-color: #fffbe6;\n  padding: 2px 4px;\n  border-radius: 3px;\n}\n\nbutton {\n  padding: 8px 12px;\n  background-color: #4a6cf7;\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 14px;\n}\n\nbutton:hover {\n  background-color: #3a5cf7;\n}\n\n#clear-log {\n  background-color: #6c757d;\n}\n\n/* Animation for events */\n@keyframes pulse {\n  0% { transform: scale(1); }\n  50% { transform: scale(1.05); }\n  100% { transform: scale(1); }\n}\n\n.event-pulse {\n  animation: pulse 0.5s ease-in-out;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Helper function to log events\nfunction logEvent(message, highlight = false) {\n  const logContainer = document.getElementById(&#39;event-log&#39;);\n  const entry = document.createElement(&#39;div&#39;);\n  entry.className = &#39;log-entry&#39;;\n\n  const time = document.createElement(&#39;div&#39;);\n  time.className = &#39;log-entry-time&#39;;\n  time.textContent = new Date().toLocaleTimeString();\n\n  const messageEl = document.createElement(&#39;div&#39;);\n  messageEl.className = &#39;log-entry-message&#39;;\n\n  if (highlight) {\n    messageEl.innerHTML = `&lt;span class=&quot;log-highlight&quot;&gt;${message}&lt;/span&gt;`;\n  } else {\n    messageEl.textContent = message;\n  }\n\n  entry.appendChild(time);\n  entry.appendChild(messageEl);\n  logContainer.appendChild(entry);\n  logContainer.scrollTop = logContainer.scrollHeight;\n}\n\n// Define the Color Picker Component\nclass ColorPickerComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Color options with names\n    this.colors = [\n    { hex: &#39;#FF5252&#39;, name: &#39;Coral Red&#39; },\n    { hex: &#39;#FFD740&#39;, name: &#39;Amber&#39; },\n    { hex: &#39;#69F0AE&#39;, name: &#39;Mint Green&#39; },\n    { hex: &#39;#40C4FF&#39;, name: &#39;Sky Blue&#39; },\n    { hex: &#39;#B388FF&#39;, name: &#39;Lavender&#39; },\n    { hex: &#39;#FF80AB&#39;, name: &#39;Pink&#39; }];\n\n\n    this.render();\n    logEvent(&#39;Color Picker component initialized&#39;);\n  }\n\n  connectedCallback() {\n    // Add event listeners to color swatches\n    const swatches = this.shadowRoot.querySelectorAll(&#39;.color-swatch&#39;);\n    swatches.forEach(swatch =&gt; {\n      swatch.addEventListener(&#39;click&#39;, this.handleColorSelect.bind(this));\n    });\n\n    logEvent(&#39;Color Picker connected to DOM&#39;);\n  }\n\n  handleColorSelect(event) {\n    const color = event.target.dataset.color;\n    const name = event.target.dataset.name;\n\n    // Highlight the selected swatch\n    const swatches = this.shadowRoot.querySelectorAll(&#39;.color-swatch&#39;);\n    swatches.forEach(swatch =&gt; {\n      if (swatch.dataset.color === color) {\n        swatch.classList.add(&#39;selected&#39;);\n      } else {\n        swatch.classList.remove(&#39;selected&#39;);\n      }\n    });\n\n    // Log internal state change\n    logEvent(`Color selected: ${name} (${color})`);\n\n    // Apply visual feedback\n    const container = this.shadowRoot.querySelector(&#39;.picker-container&#39;);\n    container.classList.add(&#39;event-pulse&#39;);\n    setTimeout(() =&gt; container.classList.remove(&#39;event-pulse&#39;), 500);\n\n    // Dispatch custom event\n    this.dispatchEvent(new CustomEvent(&#39;color-changed&#39;, {\n      bubbles: true, // Event bubbles up through the DOM\n      composed: true, // Event crosses the shadow DOM boundary\n      detail: { // Data to send with the event\n        color: color,\n        name: name } }));\n\n\n\n    // Log event dispatch\n    logEvent(`Event dispatched: color-changed with color=${color}, name=${name}`, true);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: inherit;\n        }\n        \n        .picker-container {\n          background-color: #fff;\n          border-radius: 8px;\n          padding: 15px;\n          box-shadow: 0 2px 10px rgba(0,0,0,0.05);\n          transition: all 0.3s ease;\n        }\n        \n        .color-palette {\n          display: grid;\n          grid-template-columns: repeat(3, 1fr);\n          gap: 10px;\n          margin-top: 10px;\n        }\n        \n        .color-swatch {\n          height: 50px;\n          border-radius: 6px;\n          cursor: pointer;\n          transition: transform 0.2s ease, box-shadow 0.2s ease;\n          position: relative;\n        }\n        \n        .color-swatch:hover {\n          transform: translateY(-2px);\n          box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n        }\n        \n        .color-swatch.selected {\n          box-shadow: 0 0 0 3px #333;\n        }\n        \n        .description {\n          margin-bottom: 15px;\n          font-size: 14px;\n          color: #666;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;picker-container&quot;&gt;\n        &lt;div class=&quot;description&quot;&gt;\n          Select a color to send an event to the display component\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;color-palette&quot;&gt;\n          ${this.colors.map(color =&gt; `\n            &lt;div class=&quot;color-swatch&quot; \n                 style=&quot;background-color: ${color.hex}&quot; \n                 data-color=&quot;${color.hex}&quot;\n                 data-name=&quot;${color.name}&quot;&gt;\n            &lt;/div&gt;\n          `).join(&#39;&#39;)}\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Define the Color Display Component\nclass ColorDisplayComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Default state\n    this.currentColor = &#39;#EEEEEE&#39;;\n    this.colorName = &#39;None Selected&#39;;\n\n    this.render();\n    logEvent(&#39;Color Display component initialized&#39;);\n  }\n\n  connectedCallback() {\n    // Listen for color-changed events\n    document.addEventListener(&#39;color-changed&#39;, this.handleColorChange.bind(this));\n    logEvent(&#39;Color Display connected to DOM&#39;);\n    logEvent(&#39;Event listener added for &quot;color-changed&quot; events&#39;);\n  }\n\n  disconnectedCallback() {\n    // Clean up event listeners when component is removed\n    document.removeEventListener(&#39;color-changed&#39;, this.handleColorChange.bind(this));\n    logEvent(&#39;Color Display removed from DOM&#39;);\n    logEvent(&#39;Event listener removed for &quot;color-changed&quot; events&#39;);\n  }\n\n  handleColorChange(event) {\n    // Extract color data from the event\n    const { color, name } = event.detail;\n\n    // Update internal state\n    this.currentColor = color;\n    this.colorName = name;\n\n    // Log event reception\n    logEvent(`Event received: color-changed with color=${color}, name=${name}`, true);\n\n    // Apply visual feedback\n    const container = this.shadowRoot.querySelector(&#39;.display-container&#39;);\n    container.classList.add(&#39;event-pulse&#39;);\n    setTimeout(() =&gt; container.classList.remove(&#39;event-pulse&#39;), 500);\n\n    // Update the component\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: inherit;\n        }\n        \n        .display-container {\n          background-color: #fff;\n          border-radius: 8px;\n          overflow: hidden;\n          box-shadow: 0 2px 10px rgba(0,0,0,0.05);\n          transition: all 0.3s ease;\n        }\n        \n        .color-preview {\n          height: 130px;\n          background-color: ${this.currentColor};\n          transition: background-color 0.3s ease;\n        }\n        \n        .color-info {\n          padding: 15px;\n        }\n        \n        .color-name {\n          font-size: 18px;\n          font-weight: 500;\n          margin-bottom: 5px;\n        }\n        \n        .color-hex {\n          font-family: monospace;\n          color: #666;\n          font-size: 14px;\n        }\n        \n        .help-text {\n          margin-top: 10px;\n          font-size: 12px;\n          color: #999;\n          font-style: italic;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;display-container&quot;&gt;\n        &lt;div class=&quot;color-preview&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;color-info&quot;&gt;\n          &lt;div class=&quot;color-name&quot;&gt;${this.colorName}&lt;/div&gt;\n          &lt;div class=&quot;color-hex&quot;&gt;${this.currentColor}&lt;/div&gt;\n          &lt;div class=&quot;help-text&quot;&gt;This component listens for color-changed events&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Register the custom elements\ncustomElements.define(&#39;color-picker-component&#39;, ColorPickerComponent);\ncustomElements.define(&#39;color-display-component&#39;, ColorDisplayComponent);\n\n// Setup clear log button\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  document.getElementById(&#39;clear-log&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n    document.getElementById(&#39;event-log&#39;).innerHTML = &#39;&#39;;\n    logEvent(&#39;Log cleared&#39;);\n  });\n\n  // Initial log message\n  logEvent(&#39;Demo initialized&#39;);\n  logEvent(&#39;Click on a color in the Color Picker to see event communication in action&#39;);\n});\n\n// Visual effects for the arrow when events happen\ndocument.addEventListener(&#39;color-changed&#39;, () =&gt; {\n  const arrow = document.querySelector(&#39;.event-arrow&#39;);\n  arrow.style.opacity = &#39;0.2&#39;;\n  setTimeout(() =&gt; {\n    arrow.style.opacity = &#39;1&#39;;\n  }, 50);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>In this example:</p>\n<ul>\n<li>\n<p>The color picker component dispatches a <code>color-changed</code> event when a color is selected</p>\n</li>\n<li>\n<p>The event includes detailed data in the <code>detail</code> property (color code and name)</p>\n</li>\n<li>\n<p>The display component listens for this event and updates accordingly</p>\n</li>\n<li>\n<p>Notice how <code>bubbles: true</code> and <code>composed: true</code> allow the event to cross shadow DOM boundaries</p>\n</li>\n</ul>\n<h3 id=\"api-consistency-pattern\">API Consistency Pattern<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#api-consistency-pattern\" aria-label=\"Link to this section\">#</a></h3>\n<p>Successful Web Component libraries establish consistent APIs:</p>\n<pre><code class=\"language-html\">&lt;!-- Consistent naming and behavior across components --&gt;\n&lt;sl-button variant=\"primary\" size=\"large\" disabled&gt;&lt;/sl-button&gt;\n&lt;sl-dropdown variant=\"primary\" size=\"large\" disabled&gt;&lt;/sl-dropdown&gt;\n&lt;sl-checkbox variant=\"primary\" size=\"large\" disabled&gt;&lt;/sl-checkbox&gt;\n</code></pre>\n<p>Shoelace, for example, uses shared names like <code>variant</code>, <code>size</code> and <code>disabled</code> across its components.</p>\n<h2 id=\"real-world-implementation-examples\">Real-World Implementation Examples<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#real-world-implementation-examples\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"design-system-components\">Design System Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#design-system-components\" aria-label=\"Link to this section\">#</a></h3>\n<p>Design systems often implement form controls with enhanced functionality:</p>\n<pre><code class=\"language-javascript\">class EnhancedSelect extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: inline-block;\n          position: relative;\n          width: 200px;\n        }\n        .select-container {\n          border: 1px solid var(--border-color, #ccc);\n          border-radius: 4px;\n          padding: 8px;\n        }\n        /* Additional styles... */\n      &lt;/style&gt;\n      &lt;div class=\"select-container\"&gt;\n        &lt;slot name=\"selected-option\"&gt;&lt;/slot&gt;\n        &lt;div class=\"dropdown\"&gt;\n          &lt;slot&gt;&lt;/slot&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Implementation details...\n  }\n}\n\ncustomElements.define('enhanced-select', EnhancedSelect);\n</code></pre>\n<p>Now let's explore a fully functional implementation of the EnhancedSelect component that demonstrates how design system principles are applied in practice:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Design System Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Design System Component Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;intro&quot;&gt;\n    &lt;p&gt;This enhanced select component demonstrates design system principles including theming, consistent styling, and accessibility.&lt;/p&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;demo-controls&quot;&gt;\n    &lt;div class=&quot;control-group&quot;&gt;\n      &lt;h3&gt;Design System Theming&lt;/h3&gt;\n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;theme-selector&quot;&gt;Theme:&lt;/label&gt;\n        &lt;select id=&quot;theme-selector&quot;&gt;\n          &lt;option value=&quot;light&quot;&gt;Light&lt;/option&gt;\n          &lt;option value=&quot;dark&quot;&gt;Dark&lt;/option&gt;\n          &lt;option value=&quot;brand&quot;&gt;Brand&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;density-selector&quot;&gt;Density:&lt;/label&gt;\n        &lt;select id=&quot;density-selector&quot;&gt;\n          &lt;option value=&quot;comfortable&quot;&gt;Comfortable&lt;/option&gt;\n          &lt;option value=&quot;compact&quot;&gt;Compact&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control checkbox&quot;&gt;\n        &lt;input type=&quot;checkbox&quot; id=&quot;rounded-toggle&quot; checked&gt;\n        &lt;label for=&quot;rounded-toggle&quot;&gt;Rounded Corners&lt;/label&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;control-group&quot;&gt;\n      &lt;h3&gt;Component State&lt;/h3&gt;\n      &lt;div class=&quot;control checkbox&quot;&gt;\n        &lt;input type=&quot;checkbox&quot; id=&quot;disabled-toggle&quot;&gt;\n        &lt;label for=&quot;disabled-toggle&quot;&gt;Disabled&lt;/label&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control checkbox&quot;&gt;\n        &lt;input type=&quot;checkbox&quot; id=&quot;required-toggle&quot;&gt;\n        &lt;label for=&quot;required-toggle&quot;&gt;Required&lt;/label&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;control&quot;&gt;\n        &lt;label for=&quot;size-selector&quot;&gt;Size:&lt;/label&gt;\n        &lt;select id=&quot;size-selector&quot;&gt;\n          &lt;option value=&quot;small&quot;&gt;Small&lt;/option&gt;\n          &lt;option value=&quot;medium&quot;&gt;Medium&lt;/option&gt;\n          &lt;option value=&quot;large&quot;&gt;Large&lt;/option&gt;\n        &lt;/select&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;demo-component-section&quot;&gt;\n    &lt;h3&gt;Component Example&lt;/h3&gt;\n    &lt;div class=&quot;example-wrapper&quot;&gt;\n      &lt;label for=&quot;enhanced-select-example&quot; class=&quot;example-label&quot;&gt;Choose a programming language:&lt;/label&gt;\n      &lt;enhanced-select \n        id=&quot;enhanced-select-example&quot;\n        placeholder=&quot;Select a language&quot;\n        theme=&quot;light&quot;\n        density=&quot;comfortable&quot;\n        size=&quot;medium&quot;\n        class=&quot;rounded&quot;&gt;\n        &lt;option value=&quot;js&quot;&gt;JavaScript&lt;/option&gt;\n        &lt;option value=&quot;ts&quot;&gt;TypeScript&lt;/option&gt;\n        &lt;option value=&quot;py&quot;&gt;Python&lt;/option&gt;\n        &lt;option value=&quot;java&quot;&gt;Java&lt;/option&gt;\n        &lt;option value=&quot;cs&quot;&gt;C#&lt;/option&gt;\n        &lt;option value=&quot;go&quot;&gt;Go&lt;/option&gt;\n        &lt;option value=&quot;rb&quot;&gt;Ruby&lt;/option&gt;\n        &lt;option value=&quot;rs&quot;&gt;Rust&lt;/option&gt;\n      &lt;/enhanced-select&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;selection-result&quot;&gt;\n      &lt;p&gt;Selected value: &lt;span id=&quot;selected-value&quot;&gt;None&lt;/span&gt;&lt;/p&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;design-system-tokens&quot;&gt;\n    &lt;h3&gt;Design System Tokens&lt;/h3&gt;\n    &lt;div class=&quot;token-display&quot;&gt;\n      &lt;div class=&quot;token-group&quot;&gt;\n        &lt;h4&gt;Colors&lt;/h4&gt;\n        &lt;div class=&quot;token color&quot; style=&quot;--color: var(--color-primary);&quot;&gt;--color-primary&lt;/div&gt;\n        &lt;div class=&quot;token color&quot; style=&quot;--color: var(--color-surface);&quot;&gt;--color-surface&lt;/div&gt;\n        &lt;div class=&quot;token color&quot; style=&quot;--color: var(--color-border);&quot;&gt;--color-border&lt;/div&gt;\n        &lt;div class=&quot;token color&quot; style=&quot;--color: var(--color-text);&quot;&gt;--color-text&lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;token-group&quot;&gt;\n        &lt;h4&gt;Spacing&lt;/h4&gt;\n        &lt;div class=&quot;token spacing&quot; style=&quot;--size: var(--spacing-xs);&quot;&gt;--spacing-xs&lt;/div&gt;\n        &lt;div class=&quot;token spacing&quot; style=&quot;--size: var(--spacing-sm);&quot;&gt;--spacing-sm&lt;/div&gt;\n        &lt;div class=&quot;token spacing&quot; style=&quot;--size: var(--spacing-md);&quot;&gt;--spacing-md&lt;/div&gt;\n        &lt;div class=&quot;token spacing&quot; style=&quot;--size: var(--spacing-lg);&quot;&gt;--spacing-lg&lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;token-group&quot;&gt;\n        &lt;h4&gt;Typography&lt;/h4&gt;\n        &lt;div class=&quot;token text&quot;&gt;--font-family&lt;/div&gt;\n        &lt;div class=&quot;token text&quot;&gt;--font-size&lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styling for the demo interface */\n:root {\n  /* Light theme (default) */\n  --color-primary: #4a6cf7;\n  --color-primary-hover: #3a5cf7;\n  --color-surface: #ffffff;\n  --color-surface-hover: #f5f8ff;\n  --color-border: #d0d7de;\n  --color-text: #24292f;\n  --color-text-secondary: #57606a;\n  --color-background: #f6f8fa;\n  \n  /* Spacing tokens */\n  --spacing-xs: 4px;\n  --spacing-sm: 8px;\n  --spacing-md: 16px;\n  --spacing-lg: 24px;\n  \n  /* Typography */\n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n  --font-size-sm: 14px;\n  --font-size-md: 16px;\n  --font-size-lg: 18px;\n  \n  /* Interactive states */\n  --focus-ring: 0 0 0 2px rgba(74, 108, 247, 0.4);\n  \n  /* Transitions */\n  --transition-default: all 0.2s ease;\n}\n\n/* Dark theme variables (will be applied to body) */\n.theme-dark {\n  --color-primary: #6c8eff;\n  --color-primary-hover: #819fff;\n  --color-surface: #1e1e2e;\n  --color-surface-hover: #2a2a3a;\n  --color-border: #383854;\n  --color-text: #e2e4e9;\n  --color-text-secondary: #9da3af;\n  --color-background: #141420;\n}\n\n/* Brand theme variables */\n.theme-brand {\n  --color-primary: #ff4081;\n  --color-primary-hover: #ff5b92;\n  --color-surface: #ffffff;\n  --color-surface-hover: #fff0f5;\n  --color-border: #ffacc5;\n  --color-text: #333333;\n  --color-text-secondary: #666666;\n  --color-background: #fff9fb;\n}\n\nbody {\n  font-family: var(--font-family);\n  background-color: var(--color-background);\n  color: var(--color-text);\n  transition: var(--transition-default);\n  margin: 0;\n  padding: 0;\n}\n\n.container {\n  max-width: 900px;\n  margin: 0 auto;\n  padding: 30px 20px;\n}\n\nh2 {\n  margin-top: 0;\n  color: var(--color-primary);\n  font-weight: 600;\n}\n\nh3 {\n  margin-bottom: 15px;\n  color: var(--color-text);\n  font-weight: 600;\n}\n\nh4 {\n  margin-bottom: 10px;\n  color: var(--color-text-secondary);\n  font-weight: 500;\n  font-size: 14px;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n}\n\n.intro {\n  background-color: var(--color-surface);\n  padding: var(--spacing-md);\n  border-radius: 8px;\n  margin-bottom: var(--spacing-lg);\n  border-left: 4px solid var(--color-primary);\n}\n\n.demo-controls {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--spacing-md);\n  margin-bottom: var(--spacing-lg);\n}\n\n.control-group {\n  flex: 1;\n  min-width: 280px;\n  background-color: var(--color-surface);\n  border-radius: 8px;\n  padding: var(--spacing-md);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n\n.control {\n  display: flex;\n  align-items: center;\n  margin-bottom: var(--spacing-md);\n  gap: var(--spacing-md);\n}\n\n.control label {\n  min-width: 80px;\n  font-weight: 500;\n  color: var(--color-text-secondary);\n}\n\n.control.checkbox {\n  gap: var(--spacing-sm);\n}\n\n.control.checkbox label {\n  min-width: auto;\n}\n\nselect, input[type=&quot;text&quot;] {\n  padding: 8px 12px;\n  border: 1px solid var(--color-border);\n  border-radius: 4px;\n  background-color: var(--color-surface);\n  color: var(--color-text);\n  font-family: inherit;\n  font-size: var(--font-size-sm);\n  transition: var(--transition-default);\n}\n\nselect:focus, input:focus {\n  outline: none;\n  box-shadow: var(--focus-ring);\n  border-color: var(--color-primary);\n}\n\n.demo-component-section {\n  background-color: var(--color-surface);\n  padding: var(--spacing-lg);\n  border-radius: 8px;\n  margin-bottom: var(--spacing-lg);\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n\n.example-wrapper {\n  margin-bottom: var(--spacing-md);\n}\n\n.example-label {\n  display: block;\n  margin-bottom: var(--spacing-sm);\n  font-weight: 500;\n  color: var(--color-text);\n}\n\n.selection-result {\n  margin-top: var(--spacing-lg);\n  padding: var(--spacing-md);\n  background-color: var(--color-background);\n  border-radius: 6px;\n}\n\n#selected-value {\n  font-weight: bold;\n  color: var(--color-primary);\n}\n\n.design-system-tokens {\n  background-color: var(--color-surface);\n  padding: var(--spacing-lg);\n  border-radius: 8px;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n\n.token-display {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--spacing-lg);\n}\n\n.token-group {\n  min-width: 200px;\n}\n\n.token {\n  padding: var(--spacing-sm);\n  margin-bottom: var(--spacing-sm);\n  border-radius: 4px;\n  font-family: monospace;\n  font-size: 14px;\n}\n\n.token.color {\n  background-color: var(--color);\n  color: white;\n  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);\n  padding: var(--spacing-md);\n  display: flex;\n  align-items: center;\n}\n\n.token.spacing {\n  width: 100%;\n  height: var(--size);\n  background-color: var(--color-primary);\n  display: flex;\n  align-items: center;\n  padding: 0 var(--spacing-sm);\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.token.text {\n  background-color: var(--color-background);\n  color: var(--color-text);\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the EnhancedSelect Web Component\nclass EnhancedSelect extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Component state\n    this._open = false;\n    this._selectedIndex = -1;\n    this._options = [];\n    this._focusedOptionIndex = -1;\n\n    // Bind methods\n    this._handleClickOutside = this._handleClickOutside.bind(this);\n    this._handleKeyDown = this._handleKeyDown.bind(this);\n\n    // Initial render\n    this.render();\n  }\n\n  // Specify observed attributes\n  static get observedAttributes() {\n    return [&#39;placeholder&#39;, &#39;theme&#39;, &#39;density&#39;, &#39;size&#39;, &#39;disabled&#39;, &#39;required&#39;];\n  }\n\n  // Lifecycle: component connected to DOM\n  connectedCallback() {\n    // Get child options from light DOM\n    this._processOptions();\n\n    // Add event listener for clicking outside\n    document.addEventListener(&#39;click&#39;, this._handleClickOutside);\n    document.addEventListener(&#39;keydown&#39;, this._handleKeyDown);\n\n    // Set initial selection if there&#39;s a selected option\n    const selectedOption = Array.from(this.querySelectorAll(&#39;option&#39;)).find(opt =&gt; opt.selected);\n    if (selectedOption) {\n      const index = this._options.findIndex(opt =&gt; opt.value === selectedOption.value);\n      if (index !== -1) {\n        this._selectOption(index);\n      }\n    }\n  }\n\n  // Lifecycle: component removed from DOM\n  disconnectedCallback() {\n    document.removeEventListener(&#39;click&#39;, this._handleClickOutside);\n    document.removeEventListener(&#39;keydown&#39;, this._handleKeyDown);\n  }\n\n  // Lifecycle: observed attributes changed\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (oldValue !== newValue) {\n      this.render();\n    }\n  }\n\n  // Process child options\n  _processOptions() {\n    const options = Array.from(this.querySelectorAll(&#39;option&#39;));\n    this._options = options.map(opt =&gt; ({\n      value: opt.value,\n      label: opt.textContent,\n      disabled: opt.disabled }));\n\n\n    this.render();\n  }\n\n  // Toggle dropdown\n  _toggleDropdown() {\n    if (this.disabled) return;\n\n    this._open = !this._open;\n\n    if (this._open) {\n      // Set focus to the selected option or first option\n      this._focusedOptionIndex = this._selectedIndex !== -1 ? this._selectedIndex : 0;\n    }\n\n    this.render();\n  }\n\n  // Close dropdown\n  _closeDropdown() {\n    if (this._open) {\n      this._open = false;\n      this.render();\n    }\n  }\n\n  // Select an option\n  _selectOption(index) {\n    if (index &lt; 0 || index &gt;= this._options.length || this._options[index].disabled) {\n      return;\n    }\n\n    this._selectedIndex = index;\n    this._closeDropdown();\n\n    // Dispatch change event\n    this.dispatchEvent(new CustomEvent(&#39;change&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: {\n        value: this._options[index].value,\n        label: this._options[index].label } }));\n\n\n\n    // Update the display of the selected value\n    const selectedValueDisplay = document.getElementById(&#39;selected-value&#39;);\n    if (selectedValueDisplay) {\n      selectedValueDisplay.textContent = this._options[index].label;\n    }\n  }\n\n  // Handle click outside to close dropdown\n  _handleClickOutside(event) {\n    const path = event.composedPath();\n    if (!path.includes(this) &amp;&amp; this._open) {\n      this._closeDropdown();\n    }\n  }\n\n  // Handle keyboard navigation\n  _handleKeyDown(event) {\n    if (!this._open) return;\n\n    switch (event.key) {\n      case &#39;ArrowDown&#39;:\n        event.preventDefault();\n        this._focusedOptionIndex = Math.min(this._focusedOptionIndex + 1, this._options.length - 1);\n        this.render();\n        break;\n\n      case &#39;ArrowUp&#39;:\n        event.preventDefault();\n        this._focusedOptionIndex = Math.max(this._focusedOptionIndex - 1, 0);\n        this.render();\n        break;\n\n      case &#39;Enter&#39;:\n      case &#39; &#39;:\n        event.preventDefault();\n        if (this._focusedOptionIndex &gt;= 0) {\n          this._selectOption(this._focusedOptionIndex);\n        }\n        break;\n\n      case &#39;Escape&#39;:\n        event.preventDefault();\n        this._closeDropdown();\n        break;\n\n      default:\n        break;}\n\n  }\n\n  // Getter/setters for properties\n  get value() {\n    return this._selectedIndex &gt;= 0 ? this._options[this._selectedIndex].value : &#39;&#39;;\n  }\n\n  set value(val) {\n    const index = this._options.findIndex(opt =&gt; opt.value === val);\n    if (index !== -1) {\n      this._selectOption(index);\n    }\n  }\n\n  get disabled() {\n    return this.hasAttribute(&#39;disabled&#39;);\n  }\n\n  set disabled(value) {\n    if (value) {\n      this.setAttribute(&#39;disabled&#39;, &#39;&#39;);\n    } else {\n      this.removeAttribute(&#39;disabled&#39;);\n    }\n  }\n\n  // Render the component\n  render() {\n    // Get attribute values with defaults\n    const theme = this.getAttribute(&#39;theme&#39;) || &#39;light&#39;;\n    const density = this.getAttribute(&#39;density&#39;) || &#39;comfortable&#39;;\n    const size = this.getAttribute(&#39;size&#39;) || &#39;medium&#39;;\n    const disabled = this.hasAttribute(&#39;disabled&#39;);\n    const required = this.hasAttribute(&#39;required&#39;);\n    const placeholder = this.getAttribute(&#39;placeholder&#39;) || &#39;Select an option&#39;;\n\n    // Check if the element has the rounded class\n    const isRounded = this.classList.contains(&#39;rounded&#39;);\n\n    // Component CSS\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          --select-padding-y: ${density === &#39;compact&#39; ? &#39;6px&#39; : &#39;10px&#39;};\n          --select-padding-x: ${density === &#39;compact&#39; ? &#39;10px&#39; : &#39;14px&#39;};\n          --select-font-size: ${\n    size === &#39;small&#39; ? &#39;var(--font-size-sm)&#39; :\n    size === &#39;large&#39; ? &#39;var(--font-size-lg)&#39; :\n    &#39;var(--font-size-md)&#39;\n    };\n          --select-border-radius: ${isRounded ? &#39;20px&#39; : &#39;4px&#39;};\n          \n          display: block;\n          position: relative;\n          width: 100%;\n          font-family: var(--font-family, ${getComputedStyle(document.body).fontFamily});\n          font-size: var(--select-font-size);\n          color: ${disabled ? &#39;var(--color-text-secondary, #666)&#39; : &#39;var(--color-text, #333)&#39;};\n          -webkit-tap-highlight-color: transparent;\n        }\n        \n        .select-container {\n          position: relative;\n          width: 100%;\n        }\n        \n        .select-trigger {\n          display: flex;\n          align-items: center;\n          justify-content: space-between;\n          width: 100%;\n          padding: var(--select-padding-y) var(--select-padding-x);\n          background-color: var(--color-surface, white);\n          border: 1px solid var(--color-border, #ccc);\n          border-radius: var(--select-border-radius);\n          cursor: ${disabled ? &#39;not-allowed&#39; : &#39;pointer&#39;};\n          transition: var(--transition-default, all 0.2s ease);\n          opacity: ${disabled ? &#39;0.7&#39; : &#39;1&#39;};\n          box-sizing: border-box;\n        }\n        \n        .select-trigger:hover {\n          ${!disabled ? &#39;background-color: var(--color-surface-hover, #f9f9f9);&#39; : &#39;&#39;}\n          ${!disabled ? &#39;border-color: var(--color-primary, #4a6cf7);&#39; : &#39;&#39;}\n        }\n        \n        .select-trigger:focus {\n          outline: none;\n          box-shadow: var(--focus-ring, 0 0 0 2px rgba(74, 108, 247, 0.4));\n          border-color: var(--color-primary, #4a6cf7);\n        }\n        \n        .select-value {\n          overflow: hidden;\n          text-overflow: ellipsis;\n          white-space: nowrap;\n          color: ${this._selectedIndex === -1 ? &#39;var(--color-text-secondary, #666)&#39; : &#39;var(--color-text, #333)&#39;};\n        }\n        \n        .select-icon {\n          display: flex;\n          align-items: center;\n          margin-left: 8px;\n          transition: transform 0.2s ease;\n          transform: ${this._open ? &#39;rotate(180deg)&#39; : &#39;rotate(0)&#39;};\n        }\n        \n        .select-icon svg {\n          width: 16px;\n          height: 16px;\n          fill: currentColor;\n        }\n        \n        .select-dropdown {\n          position: absolute;\n          top: calc(100% + 4px);\n          left: 0;\n          width: 100%;\n          max-height: 200px;\n          overflow-y: auto;\n          background-color: var(--color-surface, white);\n          border: 1px solid var(--color-border, #ccc);\n          border-radius: var(--select-border-radius);\n          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n          z-index: 100;\n          display: ${this._open ? &#39;block&#39; : &#39;none&#39;};\n        }\n        \n        .select-option {\n          padding: var(--select-padding-y) var(--select-padding-x);\n          cursor: pointer;\n          transition: background-color 0.2s ease;\n        }\n        \n        .select-option:hover:not(.disabled) {\n          background-color: var(--color-surface-hover, #f5f5f5);\n        }\n        \n        .select-option.focused {\n          background-color: var(--color-surface-hover, #f5f5f5);\n        }\n        \n        .select-option.selected {\n          background-color: rgba(var(--color-primary-rgb, 74, 108, 247), 0.1);\n          color: var(--color-primary, #4a6cf7);\n          font-weight: 500;\n        }\n        \n        .select-option.disabled {\n          opacity: 0.5;\n          cursor: not-allowed;\n        }\n        \n        .required-indicator {\n          color: ${theme === &#39;brand&#39; ? &#39;#ff4081&#39; : &#39;#e41c3e&#39;};\n          margin-left: 4px;\n        }\n        \n        /* Empty state */\n        .empty-message {\n          padding: var(--select-padding-y) var(--select-padding-x);\n          color: var(--color-text-secondary, #666);\n          font-style: italic;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;select-container&quot;&gt;\n        &lt;div \n          class=&quot;select-trigger&quot; \n          tabindex=&quot;${disabled ? &#39;-1&#39; : &#39;0&#39;}&quot; \n          role=&quot;combobox&quot;\n          aria-haspopup=&quot;listbox&quot;\n          aria-expanded=&quot;${this._open}&quot;\n          aria-disabled=&quot;${disabled}&quot;\n          aria-required=&quot;${required}&quot;\n          @click=&quot;${() =&gt; this._toggleDropdown()}&quot;&gt;\n          &lt;div class=&quot;select-value&quot;&gt;\n            ${this._selectedIndex !== -1 ? this._options[this._selectedIndex].label : placeholder}\n            ${required ? &#39;&lt;span class=&quot;required-indicator&quot;&gt;*&lt;/span&gt;&#39; : &#39;&#39;}\n          &lt;/div&gt;\n          &lt;div class=&quot;select-icon&quot;&gt;\n            &lt;svg viewBox=&quot;0 0 24 24&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;\n              &lt;path d=&quot;M7 10l5 5 5-5z&quot;/&gt;\n            &lt;/svg&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;select-dropdown&quot; role=&quot;listbox&quot;&gt;\n          ${this._options.length === 0 ?\n    `&lt;div class=&quot;empty-message&quot;&gt;No options available&lt;/div&gt;` :\n    this._options.map((option, index) =&gt; `\n              &lt;div \n                class=&quot;select-option ${index === this._selectedIndex ? &#39;selected&#39; : &#39;&#39;} ${index === this._focusedOptionIndex ? &#39;focused&#39; : &#39;&#39;} ${option.disabled ? &#39;disabled&#39; : &#39;&#39;}&quot; \n                role=&quot;option&quot; \n                aria-selected=&quot;${index === this._selectedIndex}&quot; \n                @click=&quot;${option.disabled ? &#39;&#39; : `this._selectOption(${index})`}&quot;&gt;\n                ${option.label}\n              &lt;/div&gt;\n            `).join(&#39;&#39;)\n    }\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Fix event listeners that were in the template string\n    this.shadowRoot.querySelectorAll(&#39;.select-trigger&#39;).forEach(el =&gt; {\n      el.addEventListener(&#39;click&#39;, () =&gt; this._toggleDropdown());\n    });\n\n    this.shadowRoot.querySelectorAll(&#39;.select-option:not(.disabled)&#39;).forEach((el, index) =&gt; {\n      el.addEventListener(&#39;click&#39;, () =&gt; this._selectOption(index));\n    });\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;enhanced-select&#39;, EnhancedSelect);\n\n// Set up theme controls\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  const component = document.querySelector(&#39;enhanced-select&#39;);\n  const selectedValue = document.getElementById(&#39;selected-value&#39;);\n\n  // Theme selector\n  document.getElementById(&#39;theme-selector&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    const theme = e.target.value;\n    component.setAttribute(&#39;theme&#39;, theme);\n\n    // Apply theme to body for demo purposes\n    document.body.className = theme === &#39;default&#39; ? &#39;&#39; : `theme-${theme}`;\n  });\n\n  // Density selector\n  document.getElementById(&#39;density-selector&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    component.setAttribute(&#39;density&#39;, e.target.value);\n  });\n\n  // Size selector\n  document.getElementById(&#39;size-selector&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    component.setAttribute(&#39;size&#39;, e.target.value);\n  });\n\n  // Rounded toggle\n  document.getElementById(&#39;rounded-toggle&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    if (e.target.checked) {\n      component.classList.add(&#39;rounded&#39;);\n    } else {\n      component.classList.remove(&#39;rounded&#39;);\n    }\n\n    // Force re-render to apply the change\n    component.render();\n  });\n\n  // Disabled toggle\n  document.getElementById(&#39;disabled-toggle&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    if (e.target.checked) {\n      component.setAttribute(&#39;disabled&#39;, &#39;&#39;);\n    } else {\n      component.removeAttribute(&#39;disabled&#39;);\n    }\n  });\n\n  // Required toggle\n  document.getElementById(&#39;required-toggle&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n    if (e.target.checked) {\n      component.setAttribute(&#39;required&#39;, &#39;&#39;);\n    } else {\n      component.removeAttribute(&#39;required&#39;);\n    }\n  });\n\n  // Listen for change events from the component\n  component.addEventListener(&#39;change&#39;, e =&gt; {\n    selectedValue.textContent = e.detail.label + ` (${e.detail.value})`;\n  });\n\n  // Initialize theme color tokens based on computed style\n  function updateTokenDisplay() {\n    document.querySelectorAll(&#39;.token.color&#39;).forEach(token =&gt; {\n      const color = getComputedStyle(token).getPropertyValue(&#39;--color&#39;);\n      // Ensure text contrast\n      const isDark = isColorDark(color);\n      token.style.color = isDark ? &#39;white&#39; : &#39;black&#39;;\n    });\n  }\n\n  // Helper to determine if a color is dark (for contrast)\n  function isColorDark(color) {\n    // Simple approach for demo purposes\n    const rgb = color.match(/\\d+/g);\n    if (!rgb) return true;\n    const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;\n    return brightness &lt; 128;\n  }\n\n  // Update tokens when theme changes\n  document.getElementById(&#39;theme-selector&#39;).addEventListener(&#39;change&#39;, updateTokenDisplay);\n\n  // Initial update\n  updateTokenDisplay();\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This enhanced select component showcases several key design system features:</p>\n<ol>\n<li>\n<p><strong>Theming</strong> - The component responds to design tokens for colors, spacing, and typography</p>\n</li>\n<li>\n<p><strong>Consistent Visual Language</strong> - The styling follows design system patterns with proper spacing and visual hierarchy</p>\n</li>\n<li>\n<p><strong>Accessibility</strong> - The component implements ARIA attributes, keyboard navigation, and focus management</p>\n</li>\n<li>\n<p><strong>Configurability</strong> - Properties like density, size, and shape variants provide flexibility within the design system</p>\n</li>\n<li>\n<p><strong>Component API</strong> - The interface follows consistent patterns with attributes and properties</p>\n</li>\n</ol>\n<p>Try changing the theme, density, and other properties to see how the component adapts while maintaining design system consistency. Using CSS custom properties as design tokens is a common way for design-system libraries to let pages restyle a shadow-DOM component.</p>\n<h3 id=\"content-widgets\">Content Widgets<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#content-widgets\" aria-label=\"Link to this section\">#</a></h3>\n<p>News sites and content platforms often use web components for interactive widgets:</p>\n<pre><code class=\"language-javascript\">class ContentRecommendation extends HTMLElement {\n  static get observedAttributes() {\n    return ['category', 'count'];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (oldValue !== newValue) {\n      this._fetchRecommendations();\n    }\n  }\n\n  connectedCallback() {\n    this._fetchRecommendations();\n  }\n\n  async _fetchRecommendations() {\n    const category = this.getAttribute('category') || 'general';\n    const count = parseInt(this.getAttribute('count') || '3', 10);\n\n    try {\n      const response = await fetch(`/api/recommendations?category=${category}&amp;count=${count}`);\n      const data = await response.json();\n      this._renderRecommendations(data);\n    } catch (error) {\n      console.error('Failed to fetch recommendations:', error);\n    }\n  }\n\n  _renderRecommendations(items) {\n    // Render logic...\n  }\n}\n\ncustomElements.define('content-recommendation', ContentRecommendation);\n</code></pre>\n<p>This pattern is used by sites like The Guardian and Washington Post for their recommendation modules.</p>\n<p>Here's a working example of a content recommendation component that demonstrates the key concepts of content widgets in web components:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Content Widget Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Content Recommendation Widget Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;controls&quot;&gt;\n    &lt;div class=&quot;control-group&quot;&gt;\n      &lt;label for=&quot;category-select&quot;&gt;Category:&lt;/label&gt;\n      &lt;select id=&quot;category-select&quot;&gt;\n        &lt;option value=&quot;technology&quot;&gt;Technology&lt;/option&gt;\n        &lt;option value=&quot;business&quot;&gt;Business&lt;/option&gt;\n        &lt;option value=&quot;health&quot;&gt;Health &amp; Wellness&lt;/option&gt;\n      &lt;/select&gt;\n      \n      &lt;label for=&quot;count-select&quot;&gt;Articles:&lt;/label&gt;\n      &lt;select id=&quot;count-select&quot;&gt;\n        &lt;option value=&quot;2&quot;&gt;2&lt;/option&gt;\n        &lt;option value=&quot;3&quot; selected&gt;3&lt;/option&gt;\n        &lt;option value=&quot;4&quot;&gt;4&lt;/option&gt;\n      &lt;/select&gt;\n      \n      &lt;button id=&quot;refresh-btn&quot;&gt;Refresh&lt;/button&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;widget-container&quot;&gt;\n    &lt;content-recommendation-widget&gt;&lt;/content-recommendation-widget&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;event-log-section&quot;&gt;\n    &lt;h3&gt;Component Events&lt;/h3&gt;\n    &lt;div id=&quot;event-log&quot;&gt;&lt;/div&gt;\n    &lt;button id=&quot;clear-log&quot;&gt;Clear Log&lt;/button&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  --color-primary: #3b82f6;\n  --color-primary-dark: #2563eb;\n  --color-surface: #ffffff;\n  --color-text: #1f2937;\n  --color-text-secondary: #6b7280;\n  --color-border: #e5e7eb;\n  --color-background: #f3f4f6;\n  --color-skeleton: #e5e7eb;\n  \n  --border-radius: 8px;\n  --spacing-xs: 4px;\n  --spacing-sm: 8px;\n  --spacing-md: 16px;\n  --spacing-lg: 24px;\n  \n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n}\n\nbody {\n  font-family: var(--font-family);\n  background-color: var(--color-background);\n  color: var(--color-text);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.container {\n  max-width: 800px;\n  margin: 0 auto;\n}\n\nh2, h3 {\n  margin-top: 0;\n  color: var(--color-text);\n}\n\n.controls {\n  background-color: var(--color-surface);\n  padding: var(--spacing-md);\n  border-radius: var(--border-radius);\n  margin-bottom: var(--spacing-lg);\n  box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n\n.control-group {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--spacing-md);\n  align-items: center;\n}\n\nlabel {\n  font-weight: 500;\n}\n\nselect, button {\n  padding: 8px 12px;\n  border-radius: 4px;\n  font-family: inherit;\n}\n\nselect {\n  border: 1px solid var(--color-border);\n  background-color: var(--color-surface);\n}\n\nbutton {\n  background-color: var(--color-primary);\n  color: white;\n  border: none;\n  cursor: pointer;\n  font-weight: 500;\n}\n\nbutton:hover {\n  background-color: var(--color-primary-dark);\n}\n\n#clear-log {\n  background-color: #6b7280;\n  margin-top: var(--spacing-sm);\n}\n\n.widget-container {\n  border-radius: var(--border-radius);\n  overflow: hidden;\n  margin-bottom: var(--spacing-lg);\n  box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n\n.event-log-section {\n  background-color: var(--color-surface);\n  padding: var(--spacing-md);\n  border-radius: var(--border-radius);\n  margin-top: var(--spacing-lg);\n}\n\n#event-log {\n  background-color: #f1f5f9;\n  padding: var(--spacing-md);\n  border-radius: 4px;\n  font-family: monospace;\n  font-size: 14px;\n  height: 150px;\n  overflow-y: auto;\n}\n\n.log-entry {\n  margin-bottom: var(--spacing-xs);\n  padding-bottom: var(--spacing-xs);\n  border-bottom: 1px solid var(--color-border);\n}\n\n.log-time {\n  color: var(--color-text-secondary);\n  font-size: 12px;\n}\n\n/* Animation for loading skeleton */\n@keyframes pulse {\n  0% { opacity: 0.6; }\n  50% { opacity: 1; }\n  100% { opacity: 0.6; }\n}\n\n.skeleton {\n  background-color: var(--color-skeleton);\n  animation: pulse 1.5s infinite;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Helper function to log events\nfunction logEvent(message) {\n  const logContainer = document.getElementById(&#39;event-log&#39;);\n  if (!logContainer) return;\n\n  const entry = document.createElement(&#39;div&#39;);\n  entry.className = &#39;log-entry&#39;;\n\n  const time = document.createElement(&#39;div&#39;);\n  time.className = &#39;log-time&#39;;\n  time.textContent = new Date().toLocaleTimeString();\n\n  const text = document.createElement(&#39;div&#39;);\n  text.textContent = message;\n\n  entry.appendChild(time);\n  entry.appendChild(text);\n  logContainer.appendChild(entry);\n  logContainer.scrollTop = logContainer.scrollHeight;\n}\n\n// Mock data for recommendations\nconst MOCK_DATA = {\n  technology: [\n  {\n    id: &#39;t1&#39;,\n    title: &#39;The Future of Web Components&#39;,\n    excerpt: &#39;Exploring how web components are transforming front-end development.&#39;,\n    image: &#39;https://picsum.photos/id/1/300/150&#39;,\n    author: &#39;Alex Chen&#39; },\n\n  {\n    id: &#39;t2&#39;,\n    title: &#39;Building with Custom Elements&#39;,\n    excerpt: &#39;A practical guide to creating reusable UI components.&#39;,\n    image: &#39;https://picsum.photos/id/2/300/150&#39;,\n    author: &#39;Maria Garcia&#39; },\n\n  {\n    id: &#39;t3&#39;,\n    title: &#39;Shadow DOM Explained&#39;,\n    excerpt: &#39;Understanding encapsulation in modern web development.&#39;,\n    image: &#39;https://picsum.photos/id/3/300/150&#39;,\n    author: &#39;James Wilson&#39; },\n\n  {\n    id: &#39;t4&#39;,\n    title: &#39;Web Components vs Frameworks&#39;,\n    excerpt: &#39;Comparing native solutions with popular frameworks.&#39;,\n    image: &#39;https://picsum.photos/id/4/300/150&#39;,\n    author: &#39;Sarah Johnson&#39; }],\n\n\n\n  business: [\n  {\n    id: &#39;b1&#39;,\n    title: &#39;Digital Transformation Strategies&#39;,\n    excerpt: &#39;How businesses are adapting to technological change.&#39;,\n    image: &#39;https://picsum.photos/id/5/300/150&#39;,\n    author: &#39;Michael Roberts&#39; },\n\n  {\n    id: &#39;b2&#39;,\n    title: &#39;Remote Work in 2025&#39;,\n    excerpt: &#39;The evolving landscape of distributed teams.&#39;,\n    image: &#39;https://picsum.photos/id/6/300/150&#39;,\n    author: &#39;Jessica Taylor&#39; },\n\n  {\n    id: &#39;b3&#39;,\n    title: &#39;Sustainable Business Practices&#39;,\n    excerpt: &#39;How environmental responsibility drives success.&#39;,\n    image: &#39;https://picsum.photos/id/7/300/150&#39;,\n    author: &#39;Robert Anderson&#39; },\n\n  {\n    id: &#39;b4&#39;,\n    title: &#39;AI in Business Intelligence&#39;,\n    excerpt: &#39;Leveraging artificial intelligence for better decisions.&#39;,\n    image: &#39;https://picsum.photos/id/8/300/150&#39;,\n    author: &#39;Emily Zhang&#39; }],\n\n\n\n  health: [\n  {\n    id: &#39;h1&#39;,\n    title: &#39;Mindfulness and Productivity&#39;,\n    excerpt: &#39;How mental wellness practices improve work performance.&#39;,\n    image: &#39;https://picsum.photos/id/9/300/150&#39;,\n    author: &#39;Dr. Elizabeth Chen&#39; },\n\n  {\n    id: &#39;h2&#39;,\n    title: &#39;Digital Health Innovations&#39;,\n    excerpt: &#39;New technologies reshaping healthcare delivery.&#39;,\n    image: &#39;https://picsum.photos/id/10/300/150&#39;,\n    author: &#39;Dr. James Peterson&#39; },\n\n  {\n    id: &#39;h3&#39;,\n    title: &#39;Nutrition Science Breakthroughs&#39;,\n    excerpt: &#39;Latest research on diet and wellness.&#39;,\n    image: &#39;https://picsum.photos/id/11/300/150&#39;,\n    author: &#39;Dr. Maria Santos&#39; },\n\n  {\n    id: &#39;h4&#39;,\n    title: &#39;Sleep Science and Performance&#39;,\n    excerpt: &#39;How quality rest affects overall health.&#39;,\n    image: &#39;https://picsum.photos/id/12/300/150&#39;,\n    author: &#39;Dr. David Kim&#39; }] };\n\n\n\n\n// ContentRecommendationWidget component\nclass ContentRecommendationWidget extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Component state\n    this.loading = true;\n    this.error = null;\n    this.articles = [];\n\n    // Initial render\n    this.render();\n  }\n\n  connectedCallback() {\n    logEvent(&#39;Component connected to DOM&#39;);\n    this.loadArticles();\n\n    // Listen for control changes\n    document.getElementById(&#39;category-select&#39;).addEventListener(&#39;change&#39;, this.handleControlChange.bind(this));\n    document.getElementById(&#39;count-select&#39;).addEventListener(&#39;change&#39;, this.handleControlChange.bind(this));\n    document.getElementById(&#39;refresh-btn&#39;).addEventListener(&#39;click&#39;, this.handleRefresh.bind(this));\n    document.getElementById(&#39;clear-log&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      document.getElementById(&#39;event-log&#39;).innerHTML = &#39;&#39;;\n    });\n  }\n\n  handleControlChange() {\n    this.loadArticles();\n  }\n\n  handleRefresh() {\n    logEvent(&#39;Refresh button clicked&#39;);\n    this.loadArticles();\n  }\n\n  // Load articles based on selected controls\n  async loadArticles() {\n    const category = document.getElementById(&#39;category-select&#39;).value;\n    const count = parseInt(document.getElementById(&#39;count-select&#39;).value);\n\n    logEvent(`Loading articles: category=${category}, count=${count}`);\n\n    this.loading = true;\n    this.error = null;\n    this.render();\n\n    try {\n      // Simulate API delay\n      await new Promise(resolve =&gt; setTimeout(resolve, 1500));\n\n      // Occasionally simulate an error for demo purposes (10% chance)\n      if (Math.random() &lt; 0.1) {\n        throw new Error(&#39;Failed to fetch recommendations. Please try again.&#39;);\n      }\n\n      // Get articles for the requested category and limit count\n      this.articles = MOCK_DATA[category]?.slice(0, count) || [];\n      this.loading = false;\n\n      logEvent(`Loaded ${this.articles.length} articles successfully`);\n\n      // Dispatch event\n      this.dispatchEvent(new CustomEvent(&#39;articles-loaded&#39;, {\n        bubbles: true,\n        composed: true,\n        detail: { articles: this.articles } }));\n\n    } catch (error) {\n      this.error = error.message;\n      this.loading = false;\n      logEvent(`Error: ${error.message}`);\n\n      // Dispatch error event\n      this.dispatchEvent(new CustomEvent(&#39;error&#39;, {\n        bubbles: true,\n        composed: true,\n        detail: { error: error.message } }));\n\n    }\n\n    this.render();\n  }\n\n  // Handle article click\n  handleArticleClick(articleId) {\n    logEvent(`Article clicked: ${articleId}`);\n\n    // Dispatch event\n    this.dispatchEvent(new CustomEvent(&#39;article-selected&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { articleId } }));\n\n  }\n\n  // Render component\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .container {\n          background: white;\n          padding: 20px;\n        }\n        \n        .title {\n          font-size: 20px;\n          font-weight: 600;\n          margin: 0 0 16px 0;\n        }\n        \n        .articles-grid {\n          display: grid;\n          grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n          gap: 16px;\n        }\n        \n        .article-card {\n          border: 1px solid #e5e7eb;\n          border-radius: 8px;\n          overflow: hidden;\n          transition: transform 0.2s, box-shadow 0.2s;\n          cursor: pointer;\n        }\n        \n        .article-card:hover {\n          transform: translateY(-4px);\n          box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);\n        }\n        \n        .article-image {\n          width: 100%;\n          height: 150px;\n          object-fit: cover;\n        }\n        \n        .article-content {\n          padding: 16px;\n        }\n        \n        .article-title {\n          font-size: 18px;\n          font-weight: 600;\n          margin: 0 0 8px 0;\n        }\n        \n        .article-excerpt {\n          font-size: 14px;\n          color: #6b7280;\n          margin: 0 0 12px 0;\n        }\n        \n        .article-author {\n          font-size: 12px;\n          color: #6b7280;\n        }\n        \n        /* Loading state */\n        .loading-container {\n          padding: 32px;\n          text-align: center;\n        }\n        \n        .spinner {\n          width: 40px;\n          height: 40px;\n          border: 4px solid rgba(0, 0, 0, 0.1);\n          border-left-color: #3b82f6;\n          border-radius: 50%;\n          animation: spin 1s linear infinite;\n          margin: 0 auto;\n        }\n        \n        .loading-text {\n          margin-top: 16px;\n          color: #6b7280;\n        }\n        \n        .skeleton-card {\n          border-radius: 8px;\n          height: 250px;\n          background: #e5e7eb;\n          animation: pulse 1.5s infinite;\n        }\n        \n        /* Error state */\n        .error-container {\n          padding: 32px;\n          text-align: center;\n          color: #ef4444;\n        }\n        \n        .retry-button {\n          margin-top: 16px;\n          padding: 8px 16px;\n          background-color: #3b82f6;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n        }\n        \n        @keyframes spin {\n          0% { transform: rotate(0deg); }\n          100% { transform: rotate(360deg); }\n        }\n        \n        @keyframes pulse {\n          0% { opacity: 0.6; }\n          50% { opacity: 1; }\n          100% { opacity: 0.6; }\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;container&quot;&gt;\n        &lt;h2 class=&quot;title&quot;&gt;Recommended For You&lt;/h2&gt;\n        \n        ${this.loading ? this.renderLoading() : &#39;&#39;}\n        ${this.error ? this.renderError() : &#39;&#39;}\n        ${!this.loading &amp;&amp; !this.error ? this.renderArticles() : &#39;&#39;}\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners after rendering\n    if (!this.loading &amp;&amp; !this.error) {\n      this.shadowRoot.querySelectorAll(&#39;.article-card&#39;).forEach(card =&gt; {\n        const articleId = card.dataset.id;\n        card.addEventListener(&#39;click&#39;, () =&gt; this.handleArticleClick(articleId));\n      });\n    }\n\n    // Add retry button listener for error state\n    if (this.error) {\n      const retryButton = this.shadowRoot.querySelector(&#39;.retry-button&#39;);\n      if (retryButton) {\n        retryButton.addEventListener(&#39;click&#39;, () =&gt; this.loadArticles());\n      }\n    }\n  }\n\n  // Render loading state\n  renderLoading() {\n    const count = parseInt(document.getElementById(&#39;count-select&#39;).value) || 3;\n\n    return `\n      &lt;div class=&quot;loading-container&quot;&gt;\n        &lt;div class=&quot;spinner&quot;&gt;&lt;/div&gt;\n        &lt;p class=&quot;loading-text&quot;&gt;Loading recommendations...&lt;/p&gt;\n        \n        &lt;div class=&quot;articles-grid&quot;&gt;\n          ${Array(count).fill(0).map(() =&gt; `\n            &lt;div class=&quot;skeleton-card&quot;&gt;&lt;/div&gt;\n          `).join(&#39;&#39;)}\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n\n  // Render error state\n  renderError() {\n    return `\n      &lt;div class=&quot;error-container&quot;&gt;\n        &lt;p&gt;${this.error}&lt;/p&gt;\n        &lt;button class=&quot;retry-button&quot;&gt;Try Again&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n  }\n\n  // Render articles grid\n  renderArticles() {\n    return `\n      &lt;div class=&quot;articles-grid&quot;&gt;\n        ${this.articles.map(article =&gt; `\n          &lt;div class=&quot;article-card&quot; data-id=&quot;${article.id}&quot;&gt;\n            &lt;img class=&quot;article-image&quot; src=&quot;${article.image}&quot; alt=&quot;${article.title}&quot;&gt;\n            &lt;div class=&quot;article-content&quot;&gt;\n              &lt;h3 class=&quot;article-title&quot;&gt;${article.title}&lt;/h3&gt;\n              &lt;p class=&quot;article-excerpt&quot;&gt;${article.excerpt}&lt;/p&gt;\n              &lt;div class=&quot;article-author&quot;&gt;By ${article.author}&lt;/div&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        `).join(&#39;&#39;)}\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;content-recommendation-widget&#39;, ContentRecommendationWidget);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This demonstration shows how content-focused components:</p>\n<ol>\n<li>\n<p><strong>Fetch and display dynamic content</strong> - Simulating API calls to retrieve articles based on user selections</p>\n</li>\n<li>\n<p><strong>Handle loading states</strong> - Using skeleton loaders to improve perceived performance</p>\n</li>\n<li>\n<p><strong>Manage error conditions</strong> - Gracefully handling failures with clear user feedback</p>\n</li>\n<li>\n<p><strong>Communicate via events</strong> - Dispatching custom events when articles are loaded or selected</p>\n</li>\n<li>\n<p><strong>Respond to configuration changes</strong> - Updating content based on category and count selections</p>\n</li>\n</ol>\n<p>This pattern is commonly used by news sites, blogs, and content platforms to create modular, reusable recommendation widgets that can be placed throughout their applications.</p>\n<p>This pattern is similar to what you'd find on content-heavy sites like The Guardian or The Washington Post, where web components encapsulate complex content widgets while maintaining clean integration with the surrounding application.</p>\n<h2 id=\"performance-patterns\">Performance Patterns<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#performance-patterns\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"lazy-loading-components\">Lazy-Loading Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#lazy-loading-components\" aria-label=\"Link to this section\">#</a></h3>\n<p>Many production Web Components implement lazy-loading to improve performance:</p>\n<pre><code class=\"language-javascript\">// Only load the component when needed\ndocument.querySelector('#show-comments').addEventListener('click', async () =&gt; {\n  // Dynamically import the component\n  await import('./components/comment-section.js');\n\n  // Now create and use it\n  const commentSection = document.createElement('comment-section');\n  document.querySelector('#comments-container').appendChild(commentSection);\n});\n</code></pre>\n<p>YouTube uses this pattern extensively to load UI components only when needed.</p>\n<p>To demonstrate the power of lazy loading web components, here's an interactive example that simulates loading resource-intensive UI components only when needed:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Lazy-Loading Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Lazy Loading Web Components Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;info-box&quot;&gt;\n    &lt;p&gt;This demo shows how lazy loading improves performance by loading heavy components only when needed. Click the buttons below to load different components.&lt;/p&gt;\n    &lt;p&gt;Each component simulates a heavy resource that would normally slow down initial page load.&lt;/p&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;performance-meter&quot;&gt;\n    &lt;div class=&quot;meter-label&quot;&gt;Page Weight:&lt;/div&gt;\n    &lt;div class=&quot;meter&quot;&gt;\n      &lt;div class=&quot;meter-bar&quot; id=&quot;page-weight-bar&quot;&gt;&lt;/div&gt;\n    &lt;/div&gt;\n    &lt;div class=&quot;meter-value&quot; id=&quot;page-weight-value&quot;&gt;10KB base&lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;components-dashboard&quot;&gt;\n    &lt;div class=&quot;component-controls&quot;&gt;\n      &lt;h3&gt;Available Components&lt;/h3&gt;\n      &lt;button class=&quot;load-button&quot; data-component=&quot;data-chart&quot;&gt;\n        &lt;span class=&quot;component-name&quot;&gt;Data Chart Component&lt;/span&gt;\n        &lt;span class=&quot;component-size&quot;&gt;+250KB&lt;/span&gt;\n      &lt;/button&gt;\n      \n      &lt;button class=&quot;load-button&quot; data-component=&quot;media-gallery&quot;&gt;\n        &lt;span class=&quot;component-name&quot;&gt;Media Gallery Component&lt;/span&gt;\n        &lt;span class=&quot;component-size&quot;&gt;+320KB&lt;/span&gt;\n      &lt;/button&gt;\n      \n      &lt;button class=&quot;load-button&quot; data-component=&quot;user-profile&quot;&gt;\n        &lt;span class=&quot;component-name&quot;&gt;User Profile Component&lt;/span&gt;\n        &lt;span class=&quot;component-size&quot;&gt;+180KB&lt;/span&gt;\n      &lt;/button&gt;\n      \n      &lt;button id=&quot;reset-demo&quot; class=&quot;reset-button&quot;&gt;\n        &lt;span&gt;Reset Demo&lt;/span&gt;\n      &lt;/button&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;component-display&quot;&gt;\n      &lt;h3&gt;Loaded Components&lt;/h3&gt;\n      &lt;div id=&quot;components-container&quot;&gt;\n        &lt;div class=&quot;placeholder-message&quot;&gt;No components loaded yet. Click a button to lazy-load a component.&lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;event-log&quot;&gt;\n    &lt;h3&gt;Loading Events&lt;/h3&gt;\n    &lt;div id=&quot;loading-log&quot;&gt;&lt;/div&gt;\n    &lt;button id=&quot;clear-log&quot;&gt;Clear Log&lt;/button&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  --color-primary: #4a6cf7;\n  --color-primary-light: #6b84f9;\n  --color-gray: #e5e7eb;\n  --color-gray-dark: #9ca3af;\n  --color-text: #1f2937;\n  --color-text-light: #6b7280;\n  --color-surface: #ffffff;\n  --color-background: #f9fafb;\n  --color-success: #10b981;\n  --color-warning: #f59e0b;\n  --color-info: #3b82f6;\n  \n  --border-radius: 8px;\n  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n  \n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: var(--font-family);\n  background-color: var(--color-background);\n  color: var(--color-text);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.container {\n  max-width: 1000px;\n  margin: 0 auto;\n}\n\nh2 {\n  margin-top: 0;\n  color: var(--color-text);\n}\n\nh3 {\n  font-size: 18px;\n  font-weight: 600;\n  margin-top: 0;\n  margin-bottom: 16px;\n}\n\n.info-box {\n  background-color: #e0f2fe;\n  border-left: 4px solid var(--color-info);\n  padding: 16px;\n  margin-bottom: 24px;\n  border-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n\n.performance-meter {\n  background-color: var(--color-surface);\n  padding: 16px;\n  margin-bottom: 24px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n}\n\n.meter-label {\n  font-weight: 600;\n  margin-bottom: 8px;\n}\n\n.meter {\n  height: 20px;\n  background-color: var(--color-gray);\n  border-radius: 10px;\n  overflow: hidden;\n  margin-bottom: 8px;\n}\n\n.meter-bar {\n  height: 100%;\n  width: 10%;\n  background-color: var(--color-success);\n  transition: width 0.5s ease;\n}\n\n.meter-value {\n  text-align: right;\n  font-size: 14px;\n  color: var(--color-text-light);\n}\n\n.components-dashboard {\n  display: flex;\n  gap: 24px;\n  margin-bottom: 24px;\n}\n\n.component-controls, .component-display {\n  flex: 1;\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n}\n\n.load-button {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n  padding: 12px 16px;\n  margin-bottom: 12px;\n  background-color: var(--color-surface);\n  border: 1px solid var(--color-gray);\n  border-radius: var(--border-radius);\n  cursor: pointer;\n  transition: all 0.2s;\n  text-align: left;\n}\n\n.load-button:hover {\n  border-color: var(--color-primary-light);\n  box-shadow: var(--shadow-sm);\n}\n\n.load-button.loaded {\n  background-color: #f0f9ff;\n  border-color: var(--color-info);\n}\n\n.component-name {\n  font-weight: 500;\n}\n\n.component-size {\n  color: var(--color-text-light);\n  font-size: 14px;\n  font-weight: normal;\n}\n\n.placeholder-message {\n  padding: 24px 16px;\n  text-align: center;\n  color: var(--color-text-light);\n  border: 2px dashed var(--color-gray);\n  border-radius: var(--border-radius);\n}\n\n.loaded-component {\n  padding: 16px;\n  margin-bottom: 16px;\n  border: 1px solid var(--color-gray);\n  border-radius: var(--border-radius);\n}\n\n.event-log {\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n}\n\n#loading-log {\n  height: 150px;\n  padding: 12px;\n  margin-bottom: 12px;\n  background-color: #f8fafc;\n  border: 1px solid var(--color-gray);\n  border-radius: var(--border-radius);\n  overflow-y: auto;\n  font-family: monospace;\n  font-size: 14px;\n}\n\n.log-entry {\n  margin-bottom: 8px;\n  padding-bottom: 8px;\n  border-bottom: 1px solid var(--color-gray);\n}\n\n.log-time {\n  color: var(--color-gray-dark);\n}\n\nbutton {\n  padding: 8px 16px;\n  background-color: var(--color-primary);\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-weight: 500;\n}\n\nbutton:hover {\n  background-color: var(--color-primary-light);\n}\n\n#clear-log {\n  background-color: var(--color-gray-dark);\n}\n\n/* Animation for loading indicator */\n@keyframes spin {\n  0% { transform: rotate(0deg); }\n  100% { transform: rotate(360deg); }\n}\n\n.loading-indicator {\n  display: inline-block;\n  width: 20px;\n  height: 20px;\n  margin-right: 8px;\n  border: 3px solid rgba(0, 0, 0, 0.1);\n  border-radius: 50%;\n  border-top-color: var(--color-primary);\n  animation: spin 1s ease-in-out infinite;\n}\n\n/* Component-specific styles */\n.chart-container {\n  height: 200px;\n  background-color: #f8fafc;\n  border-radius: var(--border-radius);\n  overflow: hidden;\n  position: relative;\n}\n\n.chart-bar {\n  position: absolute;\n  bottom: 0;\n  width: 30px;\n  background-color: var(--color-primary);\n  transition: height 0.5s ease;\n}\n\n.gallery-grid {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 8px;\n}\n\n.gallery-item {\n  aspect-ratio: 1 / 1;\n  background-color: #f1f5f9;\n  border-radius: 4px;\n  overflow: hidden;\n}\n\n.gallery-item img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.profile-container {\n  display: flex;\n  align-items: center;\n  gap: 16px;\n}\n\n.profile-avatar {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  background-color: #f1f5f9;\n  overflow: hidden;\n}\n\n.profile-avatar img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.profile-info {\n  flex: 1;\n}\n\n.profile-name {\n  font-size: 18px;\n  font-weight: 600;\n  margin-bottom: 4px;\n}\n\n.profile-details {\n  color: var(--color-text-light);\n  font-size: 14px;\n}\n\n@media (max-width: 768px) {\n  .components-dashboard {\n    flex-direction: column;\n  }\n}\n.reset-button {\n  display: block;\n  width: 100%;\n  margin-top: 20px;\n  padding: 12px 16px;\n  background-color: #6b7280;\n  color: white;\n  border: none;\n  border-radius: var(--border-radius);\n  cursor: pointer;\n  font-weight: 500;\n  transition: background-color 0.2s;\n}\n\n.reset-button:hover {\n  background-color: #4b5563;\n}\n\n.reset-button:active {\n  transform: translateY(1px);\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Helper function to log events\nfunction logEvent(message) {\n  const logContainer = document.getElementById(&#39;loading-log&#39;);\n  if (!logContainer) return;\n\n  const entry = document.createElement(&#39;div&#39;);\n  entry.className = &#39;log-entry&#39;;\n\n  const time = document.createElement(&#39;span&#39;);\n  time.className = &#39;log-time&#39;;\n  time.textContent = `[${new Date().toLocaleTimeString()}] `;\n\n  entry.appendChild(time);\n  entry.appendChild(document.createTextNode(message));\n  logContainer.appendChild(entry);\n  logContainer.scrollTop = logContainer.scrollHeight;\n}\n\n// Update the performance meter\nfunction updatePerformanceMeter(additionalKB) {\n  const totalWeight = 10 + additionalKB; // 10KB base + loaded components\n  const weightPercentage = Math.min(100, totalWeight / 800 * 100); // Cap at 100%\n\n  const weightBar = document.getElementById(&#39;page-weight-bar&#39;);\n  const weightValue = document.getElementById(&#39;page-weight-value&#39;);\n\n  weightBar.style.width = `${weightPercentage}%`;\n  weightValue.textContent = `${totalWeight}KB loaded`;\n\n  // Change color based on weight\n  if (weightPercentage &gt; 70) {\n    weightBar.style.backgroundColor = &#39;#ef4444&#39;; // Red for heavy\n  } else if (weightPercentage &gt; 40) {\n    weightBar.style.backgroundColor = &#39;#f59e0b&#39;; // Amber for medium\n  } else {\n    weightBar.style.backgroundColor = &#39;#10b981&#39;; // Green for light\n  }\n}\n\n// Simulate loading delay\nfunction simulateLoading(ms) {\n  return new Promise(resolve =&gt; setTimeout(resolve, ms));\n}\n\n// Component definitions - In a real app, these would be in separate files\nclass DataChartComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n  }\n\n  connectedCallback() {\n    logEvent(&#39;DataChartComponent: Initializing...&#39;);\n\n    // Simulate resource-intensive initialization\n    setTimeout(() =&gt; {\n      this.render();\n      logEvent(&#39;DataChartComponent: Rendered and ready&#39;);\n    }, 500);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .chart-container {\n          height: 200px;\n          background-color: #f8fafc;\n          border-radius: 8px;\n          padding: 16px;\n          position: relative;\n          display: flex;\n          align-items: flex-end;\n          justify-content: space-around;\n        }\n        \n        .chart-bar {\n          width: 40px;\n          background-color: #4a6cf7;\n          transition: height 0.5s ease;\n        }\n        \n        .title {\n          font-weight: 600;\n          margin-bottom: 12px;\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;div class=&quot;title&quot;&gt;Data Visualization Chart&lt;/div&gt;\n        &lt;div class=&quot;chart-container&quot;&gt;\n          &lt;div class=&quot;chart-bar&quot; style=&quot;height: 60%;&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;chart-bar&quot; style=&quot;height: 80%;&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;chart-bar&quot; style=&quot;height: 40%;&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;chart-bar&quot; style=&quot;height: 90%;&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;chart-bar&quot; style=&quot;height: 50%;&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;chart-bar&quot; style=&quot;height: 70%;&quot;&gt;&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Animate bars for visual effect\n    const bars = this.shadowRoot.querySelectorAll(&#39;.chart-bar&#39;);\n    bars.forEach((bar, index) =&gt; {\n      const originalHeight = bar.style.height;\n      bar.style.height = &#39;0%&#39;;\n      setTimeout(() =&gt; {\n        bar.style.height = originalHeight;\n      }, 100 * index);\n    });\n  }}\n\n\nclass MediaGalleryComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n  }\n\n  connectedCallback() {\n    logEvent(&#39;MediaGalleryComponent: Initializing...&#39;);\n\n    // Simulate resource-intensive initialization\n    setTimeout(() =&gt; {\n      this.render();\n      logEvent(&#39;MediaGalleryComponent: Rendered and ready&#39;);\n    }, 700);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .title {\n          font-weight: 600;\n          margin-bottom: 12px;\n        }\n        \n        .gallery-grid {\n          display: grid;\n          grid-template-columns: repeat(3, 1fr);\n          gap: 8px;\n        }\n        \n        .gallery-item {\n          aspect-ratio: 1 / 1;\n          background-color: #f1f5f9;\n          border-radius: 4px;\n          overflow: hidden;\n          opacity: 0;\n          transform: scale(0.9);\n          transition: opacity 0.3s ease, transform 0.3s ease;\n        }\n        \n        .gallery-item img {\n          width: 100%;\n          height: 100%;\n          object-fit: cover;\n        }\n      &lt;/style&gt;\n      \n      &lt;div&gt;\n        &lt;div class=&quot;title&quot;&gt;Media Gallery&lt;/div&gt;\n        &lt;div class=&quot;gallery-grid&quot;&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/1/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/2/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/3/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/4/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/5/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/6/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/7/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/8/200&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;gallery-item&quot;&gt;&lt;img src=&quot;https://picsum.photos/id/9/200&quot;&gt;&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Animate items appearing for visual effect\n    const items = this.shadowRoot.querySelectorAll(&#39;.gallery-item&#39;);\n    items.forEach((item, index) =&gt; {\n      setTimeout(() =&gt; {\n        item.style.opacity = &#39;1&#39;;\n        item.style.transform = &#39;scale(1)&#39;;\n      }, 100 * index);\n    });\n  }}\n\n\nclass UserProfileComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n  }\n\n  connectedCallback() {\n    logEvent(&#39;UserProfileComponent: Initializing...&#39;);\n\n    // Simulate resource-intensive initialization\n    setTimeout(() =&gt; {\n      this.render();\n      logEvent(&#39;UserProfileComponent: Rendered and ready&#39;);\n    }, 600);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .profile-container {\n          display: flex;\n          align-items: center;\n          gap: 16px;\n        }\n        \n        .profile-avatar {\n          width: 80px;\n          height: 80px;\n          border-radius: 50%;\n          background-color: #f1f5f9;\n          overflow: hidden;\n          opacity: 0;\n          transform: translateY(10px);\n          transition: opacity 0.3s ease, transform 0.3s ease;\n        }\n        \n        .profile-avatar img {\n          width: 100%;\n          height: 100%;\n          object-fit: cover;\n        }\n        \n        .profile-info {\n          flex: 1;\n          opacity: 0;\n          transform: translateY(10px);\n          transition: opacity 0.3s ease, transform 0.3s ease;\n          transition-delay: 0.2s;\n        }\n        \n        .profile-name {\n          font-size: 18px;\n          font-weight: 600;\n          margin-bottom: 4px;\n        }\n        \n        .profile-details {\n          color: #6b7280;\n          font-size: 14px;\n          margin-bottom: 8px;\n        }\n        \n        .profile-bio {\n          font-size: 14px;\n          line-height: 1.5;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;profile-container&quot;&gt;\n        &lt;div class=&quot;profile-avatar&quot;&gt;\n          &lt;img src=&quot;https://randomuser.me/api/portraits/women/42.jpg&quot;&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;profile-info&quot;&gt;\n          &lt;div class=&quot;profile-name&quot;&gt;Sarah Johnson&lt;/div&gt;\n          &lt;div class=&quot;profile-details&quot;&gt;Front-end Developer • San Francisco&lt;/div&gt;\n          &lt;div class=&quot;profile-bio&quot;&gt;\n            Web component specialist with 5+ years of experience building scalable, \n            accessible UI components for enterprise applications.\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Animate profile appearing for visual effect\n    setTimeout(() =&gt; {\n      this.shadowRoot.querySelector(&#39;.profile-avatar&#39;).style.opacity = &#39;1&#39;;\n      this.shadowRoot.querySelector(&#39;.profile-avatar&#39;).style.transform = &#39;translateY(0)&#39;;\n\n      this.shadowRoot.querySelector(&#39;.profile-info&#39;).style.opacity = &#39;1&#39;;\n      this.shadowRoot.querySelector(&#39;.profile-info&#39;).style.transform = &#39;translateY(0)&#39;;\n    }, 100);\n  }}\n\n\n// Track loaded components\nconst loadedComponents = {\n  &#39;data-chart&#39;: false,\n  &#39;media-gallery&#39;: false,\n  &#39;user-profile&#39;: false };\n\n\n// Handle component loading\nasync function loadComponent(componentName) {\n  // Check if already loaded\n  if (loadedComponents[componentName]) {\n    logEvent(`${componentName} is already loaded`);\n    return;\n  }\n\n  const componentsContainer = document.getElementById(&#39;components-container&#39;);\n  const componentSizes = {\n    &#39;data-chart&#39;: 250,\n    &#39;media-gallery&#39;: 320,\n    &#39;user-profile&#39;: 180 };\n\n\n  // Remove placeholder if present\n  const placeholder = componentsContainer.querySelector(&#39;.placeholder-message&#39;);\n  if (placeholder) {\n    placeholder.remove();\n  }\n\n  // Create component container\n  const componentContainer = document.createElement(&#39;div&#39;);\n  componentContainer.className = &#39;loaded-component&#39;;\n  componentContainer.innerHTML = `\n    &lt;div class=&quot;loading-indicator&quot;&gt;&lt;/div&gt;\n    &lt;span&gt;Loading ${componentName}...&lt;/span&gt;\n  `;\n  componentsContainer.prepend(componentContainer);\n\n  // Mark button as loading\n  const button = document.querySelector(`[data-component=&quot;${componentName}&quot;]`);\n  button.disabled = true;\n  button.innerHTML = `\n    &lt;span class=&quot;component-name&quot;&gt;Loading ${componentName}...&lt;/span&gt;\n    &lt;span class=&quot;component-size&quot;&gt;+${componentSizes[componentName]}KB&lt;/span&gt;\n  `;\n\n  // Log loading start\n  logEvent(`Started loading ${componentName}`);\n\n  try {\n    // In a real app, this would be a dynamic import\n    // await import(`./components/${componentName}.js`);\n\n    // Simulate loading delay based on component size\n    const loadTime = componentSizes[componentName] / 50; // ~5-6 seconds for largest component\n    await simulateLoading(loadTime);\n\n    // Register custom element if not already registered\n    if (!customElements.get(componentName)) {\n      switch (componentName) {\n        case &#39;data-chart&#39;:\n          customElements.define(&#39;data-chart&#39;, DataChartComponent);\n          break;\n        case &#39;media-gallery&#39;:\n          customElements.define(&#39;media-gallery&#39;, MediaGalleryComponent);\n          break;\n        case &#39;user-profile&#39;:\n          customElements.define(&#39;user-profile&#39;, UserProfileComponent);\n          break;}\n\n    }\n\n    // Update component container\n    componentContainer.innerHTML = `&lt;${componentName}&gt;&lt;/${componentName}&gt;`;\n\n    // Mark as loaded\n    loadedComponents[componentName] = true;\n    button.disabled = false;\n    button.classList.add(&#39;loaded&#39;);\n    button.innerHTML = `\n      &lt;span class=&quot;component-name&quot;&gt;${componentName} (Loaded)&lt;/span&gt;\n      &lt;span class=&quot;component-size&quot;&gt;+${componentSizes[componentName]}KB&lt;/span&gt;\n    `;\n\n    // Update performance meter\n    let totalLoaded = 0;\n    for (const component in loadedComponents) {\n      if (loadedComponents[component]) {\n        totalLoaded += componentSizes[component];\n      }\n    }\n    updatePerformanceMeter(totalLoaded);\n\n    // Log loading completion\n    logEvent(`Successfully loaded ${componentName} (+${componentSizes[componentName]}KB)`);\n  } catch (error) {\n    componentContainer.innerHTML = `\n      &lt;div style=&quot;color: #ef4444;&quot;&gt;Error loading ${componentName}: ${error.message}&lt;/div&gt;\n    `;\n    button.disabled = false;\n    logEvent(`Error loading ${componentName}: ${error.message}`);\n  }\n}\n\n// Initialize the demo\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  // Set up click handlers for load buttons\n  document.querySelectorAll(&#39;.load-button&#39;).forEach(button =&gt; {\n    const componentName = button.dataset.component;\n    button.addEventListener(&#39;click&#39;, () =&gt; loadComponent(componentName));\n  });\n\n  // Clear log button\n  document.getElementById(&#39;clear-log&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n    document.getElementById(&#39;loading-log&#39;).innerHTML = &#39;&#39;;\n  });\n\n  // Log startup\n  logEvent(&#39;Demo initialized - 10KB base page loaded&#39;);\n  logEvent(&#39;Components will be lazy-loaded on demand&#39;);\n});\n\n// Reset demo function\nfunction resetDemo() {\n  // Log reset event\n  logEvent(&#39;Resetting demo...&#39;);\n\n  // Reset loaded components tracking\n  for (const component in loadedComponents) {\n    loadedComponents[component] = false;\n  }\n\n  // Reset performance meter\n  updatePerformanceMeter(0);\n\n  // Reset component buttons\n  document.querySelectorAll(&#39;.load-button&#39;).forEach(button =&gt; {\n    button.classList.remove(&#39;loaded&#39;);\n    button.disabled = false;\n\n    const componentName = button.dataset.component;\n    const componentSize = {\n      &#39;data-chart&#39;: 250,\n      &#39;media-gallery&#39;: 320,\n      &#39;user-profile&#39;: 180 }[\n    componentName];\n\n    button.innerHTML = `\n      &lt;span class=&quot;component-name&quot;&gt;${componentName}&lt;/span&gt;\n      &lt;span class=&quot;component-size&quot;&gt;+${componentSize}KB&lt;/span&gt;\n    `;\n  });\n\n  // Clear components container and add placeholder\n  const componentsContainer = document.getElementById(&#39;components-container&#39;);\n  componentsContainer.innerHTML = `\n    &lt;div class=&quot;placeholder-message&quot;&gt;No components loaded yet. Click a button to lazy-load a component.&lt;/div&gt;\n  `;\n\n  // Log completion\n  logEvent(&#39;Demo reset complete - Back to 10KB base page&#39;);\n}\n\n// Initialize the demo\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  // Set up click handlers for load buttons\n  document.querySelectorAll(&#39;.load-button&#39;).forEach(button =&gt; {\n    const componentName = button.dataset.component;\n    button.addEventListener(&#39;click&#39;, () =&gt; loadComponent(componentName));\n  });\n\n  // Reset button handler\n  document.getElementById(&#39;reset-demo&#39;).addEventListener(&#39;click&#39;, resetDemo);\n\n  // Clear log button\n  document.getElementById(&#39;clear-log&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n    document.getElementById(&#39;loading-log&#39;).innerHTML = &#39;&#39;;\n  });\n\n  // Log startup\n  logEvent(&#39;Demo initialized - 10KB base page loaded&#39;);\n  logEvent(&#39;Components will be lazy-loaded on demand&#39;);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This demo shows how lazy loading can dramatically improve initial page load performance by:</p>\n<ol>\n<li>\n<p><strong>Reducing initial page weight</strong> - Only the core 10KB of code loads initially</p>\n</li>\n<li>\n<p><strong>Loading components on demand</strong> - Each component is loaded only when requested</p>\n</li>\n<li>\n<p><strong>Providing visual feedback</strong> - The performance meter shows the impact of each loaded component</p>\n</li>\n<li>\n<p><strong>Using dynamic imports</strong> - Components are loaded asynchronously without blocking the main thread</p>\n</li>\n</ol>\n<p>Try clicking the buttons to load different components and watch how the page weight increases only when new components are added. This pattern is used extensively by YouTube for their video player interface, where heavy components are loaded only when the user interacts with them.</p>\n<h3 id=\"constructable-stylesheets\">Constructable Stylesheets<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#constructable-stylesheets\" aria-label=\"Link to this section\">#</a></h3>\n<p>For components used multiple times on a page, shared stylesheets improve performance:</p>\n<pre><code class=\"language-javascript\">// Create once, use many times\nconst sheet = new CSSStyleSheet();\nsheet.replaceSync(`\n  :host {\n    display: block;\n    padding: 16px;\n  }\n  .content {\n    color: var(--text-color, #333);\n  }\n`);\n\nclass ReusableCard extends HTMLElement {\n  constructor() {\n    super();\n    const shadow = this.attachShadow({ mode: 'open' });\n    // Use the shared stylesheet\n    shadow.adoptedStyleSheets = [sheet];\n    shadow.innerHTML = `&lt;div class=\"content\"&gt;&lt;slot&gt;&lt;/slot&gt;&lt;/div&gt;`;\n  }\n}\n</code></pre>\n<p>To better understand the performance benefits of Constructable Stylesheets, this interactive demo demonstrates the difference between traditional inline styles (duplicated for each component) and shared stylesheets (reused across all components):</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Constructable Stylesheet Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Constructable Stylesheets Performance Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;info-box&quot;&gt;\n    &lt;p&gt;&lt;strong&gt;Constructable Stylesheets&lt;/strong&gt; allow Web Components to share a single stylesheet across multiple instances, improving performance and memory usage.&lt;/p&gt;\n    &lt;p&gt;This demo compares traditional inline styles (each component has its own copy) vs. Constructable Stylesheets (all components share one stylesheet).&lt;/p&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;controls&quot;&gt;\n    &lt;div class=&quot;method-selector&quot;&gt;\n      &lt;h3&gt;Select Styling Method:&lt;/h3&gt;\n      &lt;div class=&quot;toggle-container&quot;&gt;\n        &lt;label class=&quot;toggle&quot;&gt;\n          &lt;input type=&quot;radio&quot; name=&quot;style-method&quot; value=&quot;traditional&quot; checked&gt;\n          &lt;span class=&quot;toggle-label&quot;&gt;Traditional&lt;/span&gt;\n          &lt;span class=&quot;description&quot;&gt;Each component has its own copy of styles&lt;/span&gt;\n        &lt;/label&gt;\n        \n        &lt;label class=&quot;toggle&quot;&gt;\n          &lt;input type=&quot;radio&quot; name=&quot;style-method&quot; value=&quot;constructable&quot;&gt;\n          &lt;span class=&quot;toggle-label&quot;&gt;Constructable Stylesheets&lt;/span&gt;\n          &lt;span class=&quot;description&quot;&gt;All components share one stylesheet&lt;/span&gt;\n        &lt;/label&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;component-controls&quot;&gt;\n      &lt;h3&gt;Add Components:&lt;/h3&gt;\n      &lt;div class=&quot;button-group&quot;&gt;\n        &lt;button id=&quot;add-10&quot; class=&quot;btn&quot;&gt;Add 10&lt;/button&gt;\n        &lt;button id=&quot;add-50&quot; class=&quot;btn&quot;&gt;Add 50&lt;/button&gt;\n        &lt;button id=&quot;add-100&quot; class=&quot;btn&quot;&gt;Add 100&lt;/button&gt;\n        &lt;button id=&quot;clear&quot; class=&quot;btn btn-secondary&quot;&gt;Clear All&lt;/button&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;metrics&quot;&gt;\n    &lt;div class=&quot;metric-box&quot;&gt;\n      &lt;h4&gt;Component Count&lt;/h4&gt;\n      &lt;div class=&quot;metric-value&quot; id=&quot;component-count&quot;&gt;0&lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;metric-box&quot;&gt;\n      &lt;h4&gt;Memory Usage&lt;/h4&gt;\n      &lt;div class=&quot;metric-value&quot; id=&quot;memory-usage&quot;&gt;-&lt;/div&gt;\n    &lt;/div&gt;\n    \n    &lt;div class=&quot;metric-box&quot;&gt;\n      &lt;h4&gt;Creation Time&lt;/h4&gt;\n      &lt;div class=&quot;metric-value&quot; id=&quot;creation-time&quot;&gt;-&lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;component-display&quot;&gt;\n    &lt;h3&gt;Component Container &lt;span id=&quot;display-method&quot; class=&quot;method-tag traditional&quot;&gt;Traditional Method&lt;/span&gt;&lt;/h3&gt;\n    &lt;div id=&quot;components-container&quot; class=&quot;components-grid&quot;&gt;&lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;explanation&quot;&gt;\n    &lt;h3&gt;How It Works&lt;/h3&gt;\n    &lt;div class=&quot;code-comparison&quot;&gt;\n      &lt;div class=&quot;code-block&quot;&gt;\n        &lt;h4&gt;Traditional Approach&lt;/h4&gt;\n        &lt;pre&gt;&lt;code&gt;// Each component has its own copy of styles\nfunction createComponent() {\n  const shadow = this.attachShadow({mode: &#39;open&#39;});\n  \n  // Styles duplicated for EACH component\n  shadow.innerHTML = `\n    &amp;lt;style&amp;gt;\n      .card { \n        background: white;\n        box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n        /* More styles... */\n      }\n    &amp;lt;/style&amp;gt;\n    &amp;lt;div class=&quot;card&quot;&amp;gt;Component content&amp;lt;/div&amp;gt;\n  `;\n}&lt;/code&gt;&lt;/pre&gt;\n      &lt;/div&gt;\n      \n      &lt;div class=&quot;code-block&quot;&gt;\n        &lt;h4&gt;Constructable Stylesheets&lt;/h4&gt;\n        &lt;pre&gt;&lt;code&gt;// Create stylesheet ONCE, share across all components\nconst sheet = new CSSStyleSheet();\nsheet.replaceSync(`\n  .card { \n    background: white;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n    /* More styles... */\n  }\n`);\n\nfunction createComponent() {\n  const shadow = this.attachShadow({mode: &#39;open&#39;});\n  \n  // Reuse the same stylesheet\n  shadow.adoptedStyleSheets = [sheet];\n  shadow.innerHTML = `&amp;lt;div class=&quot;card&quot;&amp;gt;Component content&amp;lt;/div&amp;gt;`;\n}&lt;/code&gt;&lt;/pre&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  --color-primary: #4a6cf7;\n  --color-primary-light: #e6ecfe;\n  --color-traditional: #f97316;\n  --color-constructable: #10b981;\n  --color-gray: #e5e7eb;\n  --color-gray-dark: #9ca3af;\n  --color-text: #1f2937;\n  --color-text-light: #6b7280;\n  --color-surface: #ffffff;\n  --color-background: #f9fafb;\n  \n  --border-radius: 8px;\n  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n  \n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: var(--font-family);\n  background-color: var(--color-background);\n  color: var(--color-text);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.container {\n  max-width: 1000px;\n  margin: 0 auto;\n}\n\nh2 {\n  font-size: 24px;\n  margin-top: 0;\n  margin-bottom: 24px;\n}\n\nh3 {\n  font-size: 18px;\n  margin-top: 0;\n  margin-bottom: 12px;\n}\n\nh4 {\n  font-size: 16px;\n  margin-top: 0;\n  margin-bottom: 8px;\n}\n\n.info-box {\n  background-color: var(--color-primary-light);\n  border-left: 4px solid var(--color-primary);\n  padding: 16px;\n  margin-bottom: 24px;\n  border-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n\n.controls {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n  margin-bottom: 24px;\n}\n\n.method-selector, .component-controls {\n  flex: 1;\n  min-width: 300px;\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n}\n\n.toggle-container {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.toggle {\n  display: flex;\n  flex-direction: column;\n  position: relative;\n  padding: 12px;\n  border: 1px solid var(--color-gray);\n  border-radius: var(--border-radius);\n  cursor: pointer;\n  transition: all 0.2s;\n}\n\n.toggle:hover {\n  border-color: var(--color-primary);\n}\n\n.toggle input[type=&quot;radio&quot;] {\n  position: absolute;\n  opacity: 0;\n  width: 0;\n  height: 0;\n}\n\n.toggle input[type=&quot;radio&quot;]:checked + .toggle-label {\n  color: var(--color-primary);\n  font-weight: 600;\n}\n\n.toggle input[type=&quot;radio&quot;]:checked + .toggle-label::before {\n  content: &#39;✓ &#39;;\n}\n\n.toggle-label {\n  font-weight: 500;\n  margin-bottom: 4px;\n}\n\n.description {\n  font-size: 14px;\n  color: var(--color-text-light);\n}\n\n.toggle:has(input[value=&quot;traditional&quot;]:checked) {\n  background-color: rgba(249, 115, 22, 0.1);\n  border-color: var(--color-traditional);\n}\n\n.toggle:has(input[value=&quot;constructable&quot;]:checked) {\n  background-color: rgba(16, 185, 129, 0.1);\n  border-color: var(--color-constructable);\n}\n\n.button-group {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n}\n\n.btn {\n  padding: 8px 16px;\n  background-color: var(--color-primary);\n  color: white;\n  border: none;\n  border-radius: 4px;\n  cursor: pointer;\n  font-family: inherit;\n  font-weight: 500;\n  transition: background-color 0.2s;\n}\n\n.btn:hover {\n  background-color: #3a5cf7;\n}\n\n.btn-secondary {\n  background-color: var(--color-gray-dark);\n}\n\n.btn-secondary:hover {\n  background-color: #8b97a7;\n}\n\n.metrics {\n  display: flex;\n  gap: 16px;\n  margin-bottom: 24px;\n  flex-wrap: wrap;\n}\n\n.metric-box {\n  flex: 1;\n  min-width: 200px;\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n  text-align: center;\n}\n\n.metric-box h4 {\n  margin: 0 0 8px 0;\n  font-weight: 500;\n  color: var(--color-text-light);\n}\n\n.metric-value {\n  font-size: 24px;\n  font-weight: 600;\n  color: var(--color-primary);\n}\n\n.component-display {\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n  margin-bottom: 24px;\n}\n\n.method-tag {\n  font-size: 14px;\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-weight: normal;\n  vertical-align: middle;\n}\n\n.method-tag.traditional {\n  background-color: rgba(249, 115, 22, 0.1);\n  color: var(--color-traditional);\n}\n\n.method-tag.constructable {\n  background-color: rgba(16, 185, 129, 0.1);\n  color: var(--color-constructable);\n}\n\n.components-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n  gap: 16px;\n  min-height: 200px;\n  margin-top: 16px;\n  padding: 16px;\n  border: 2px dashed var(--color-gray);\n  border-radius: var(--border-radius);\n}\n\n.empty-message {\n  grid-column: 1 / -1;\n  text-align: center;\n  color: var(--color-text-light);\n  padding: 40px 0;\n}\n\n.explanation {\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n}\n\n.code-comparison {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 16px;\n  margin-top: 16px;\n}\n\n.code-block {\n  flex: 1;\n  min-width: 300px;\n  background-color: var(--color-background);\n  border-radius: var(--border-radius);\n  overflow: hidden;\n}\n\n.code-block h4 {\n  margin: 0;\n  padding: 12px 16px;\n  background-color: #f1f5f9;\n  border-bottom: 1px solid var(--color-gray);\n}\n\n.code-block pre {\n  margin: 0;\n  padding: 16px;\n  font-family: monospace;\n  font-size: 14px;\n  overflow-x: auto;\n  background-color: #f8fafc;\n}\n\n/* Styling for the actual demo components */\n.demo-component {\n  border-radius: var(--border-radius);\n  overflow: hidden;\n  transition: transform 0.2s;\n}\n\n.demo-component:hover {\n  transform: translateY(-4px);\n}\n\n.demo-component.traditional {\n  box-shadow: 0 2px 8px rgba(249, 115, 22, 0.2);\n}\n\n.demo-component.constructable {\n  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);\n}\n\n@media (max-width: 768px) {\n  .controls {\n    flex-direction: column;\n  }\n  \n  .code-comparison {\n    flex-direction: column;\n  }\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Initialize shared stylesheet for the Constructable Stylesheets approach\nconst sharedStyleSheet = new CSSStyleSheet();\nsharedStyleSheet.replaceSync(`\n  .card {\n    background-color: white;\n    border-radius: 8px;\n    padding: 16px;\n    height: 120px;\n    display: flex;\n    flex-direction: column;\n    justify-content: space-between;\n    transition: transform 0.2s, box-shadow 0.2s;\n  }\n  \n  .card:hover {\n    transform: translateY(-4px);\n  }\n  \n  .card-header {\n    font-weight: 600;\n    margin-bottom: 8px;\n  }\n  \n  .card-content {\n    font-size: 14px;\n    color: #6b7280;\n    flex-grow: 1;\n  }\n  \n  .card-footer {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    font-size: 12px;\n    color: #9ca3af;\n    margin-top: 8px;\n  }\n`);\n\n// Track component count and styling method\nlet componentCount = 0;\nlet currentMethod = &#39;traditional&#39;;\n\n// Define the traditional component (each instance has its own styles)\nclass TraditionalComponent extends HTMLElement {\n  constructor() {\n    super();\n    const shadow = this.attachShadow({ mode: &#39;open&#39; });\n\n    // Each instance gets its own copy of the styles\n    shadow.innerHTML = `\n      &lt;style&gt;\n        .card {\n          background-color: white;\n          border-radius: 8px;\n          padding: 16px;\n          height: 120px;\n          display: flex;\n          flex-direction: column;\n          justify-content: space-between;\n          transition: transform 0.2s, box-shadow 0.2s;\n        }\n        \n        .card:hover {\n          transform: translateY(-4px);\n        }\n        \n        .card-header {\n          font-weight: 600;\n          margin-bottom: 8px;\n        }\n        \n        .card-content {\n          font-size: 14px;\n          color: #6b7280;\n          flex-grow: 1;\n        }\n        \n        .card-footer {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          font-size: 12px;\n          color: #9ca3af;\n          margin-top: 8px;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;card&quot;&gt;\n        &lt;div class=&quot;card-header&quot;&gt;Component #${++componentCount}&lt;/div&gt;\n        &lt;div class=&quot;card-content&quot;&gt;This component has its own copy of styles (${this.getBytesCount()} bytes)&lt;/div&gt;\n        &lt;div class=&quot;card-footer&quot;&gt;\n          &lt;span&gt;Traditional Method&lt;/span&gt;\n          &lt;span&gt;ID: ${Math.floor(Math.random() * 1000)}&lt;/span&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n\n  getBytesCount() {\n    // Estimate the size of styles in bytes\n    return 450; // Approximation of the CSS size\n  }}\n\n\n// Define the optimized component (shares stylesheet)\nclass OptimizedComponent extends HTMLElement {\n  constructor() {\n    super();\n    const shadow = this.attachShadow({ mode: &#39;open&#39; });\n\n    // Use the shared stylesheet\n    shadow.adoptedStyleSheets = [sharedStyleSheet];\n\n    // Only HTML content, no styles\n    shadow.innerHTML = `\n      &lt;div class=&quot;card&quot;&gt;\n        &lt;div class=&quot;card-header&quot;&gt;Component #${++componentCount}&lt;/div&gt;\n        &lt;div class=&quot;card-content&quot;&gt;This component shares styles (0 bytes added)&lt;/div&gt;\n        &lt;div class=&quot;card-footer&quot;&gt;\n          &lt;span&gt;Constructable Stylesheet&lt;/span&gt;\n          &lt;span&gt;ID: ${Math.floor(Math.random() * 1000)}&lt;/span&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n\n  getBytesCount() {\n    // No additional style bytes since it uses the shared stylesheet\n    return 0;\n  }}\n\n\n// Register custom elements\ncustomElements.define(&#39;traditional-component&#39;, TraditionalComponent);\ncustomElements.define(&#39;optimized-component&#39;, OptimizedComponent);\n\n// Helper functions\nfunction updateMetrics() {\n  document.getElementById(&#39;component-count&#39;).textContent = componentCount;\n\n  // Calculate estimated memory usage\n  let memoryUsage = 10; // Base page KB\n  if (currentMethod === &#39;traditional&#39;) {\n    memoryUsage += componentCount * 0.45; // Each component adds ~450 bytes of CSS\n  } else {\n    memoryUsage += 0.45; // Just one copy of styles shared by all components\n  }\n\n  document.getElementById(&#39;memory-usage&#39;).textContent = `~${memoryUsage.toFixed(1)} KB`;\n}\n\nfunction clearComponents() {\n  const container = document.getElementById(&#39;components-container&#39;);\n  container.innerHTML = &#39;&lt;div class=&quot;empty-message&quot;&gt;No components added yet. Click a button above to add components.&lt;/div&gt;&#39;;\n  componentCount = 0;\n  updateMetrics();\n  document.getElementById(&#39;creation-time&#39;).textContent = &#39;-&#39;;\n}\n\nfunction addComponents(count) {\n  const container = document.getElementById(&#39;components-container&#39;);\n\n  // Remove empty message if present\n  const emptyMessage = container.querySelector(&#39;.empty-message&#39;);\n  if (emptyMessage) {\n    emptyMessage.remove();\n  }\n\n  // Measure creation time\n  const startTime = performance.now();\n\n  // Create elements\n  for (let i = 0; i &lt; count; i++) {\n    const component = currentMethod === &#39;traditional&#39; ?\n    document.createElement(&#39;traditional-component&#39;) :\n    document.createElement(&#39;optimized-component&#39;);\n\n    component.className = `demo-component ${currentMethod}`;\n    container.appendChild(component);\n  }\n\n  // Calculate creation time\nconst endTime = performance.now();\n  const creationTime = endTime - startTime;\n  document.getElementById(&#39;creation-time&#39;).textContent = `${creationTime.toFixed(2)} ms`;\n\n  // Update metrics\n  updateMetrics();\n}\n\nfunction switchMethod(method) {\n  currentMethod = method;\n  document.getElementById(&#39;display-method&#39;).textContent =\n  method === &#39;traditional&#39; ? &#39;Traditional Method&#39; : &#39;Constructable Stylesheets&#39;;\n  document.getElementById(&#39;display-method&#39;).className =\n  `method-tag ${method}`;\n\n  // Clear existing components\n  clearComponents();\n}\n\n// Set up event listeners\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  // Method selection\n  document.querySelectorAll(&#39;input[name=&quot;style-method&quot;]&#39;).forEach(radio =&gt; {\n    radio.addEventListener(&#39;change&#39;, e =&gt; {\n      switchMethod(e.target.value);\n    });\n  });\n\n  // Add component buttons\n  document.getElementById(&#39;add-10&#39;).addEventListener(&#39;click&#39;, () =&gt; addComponents(10));\n  document.getElementById(&#39;add-50&#39;).addEventListener(&#39;click&#39;, () =&gt; addComponents(50));\n  document.getElementById(&#39;add-100&#39;).addEventListener(&#39;click&#39;, () =&gt; addComponents(100));\n\n  // Clear button\n  document.getElementById(&#39;clear&#39;).addEventListener(&#39;click&#39;, clearComponents);\n\n  // Initialize with empty message\n  clearComponents();\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>Try adding multiple components with both methods to see how Constructable Stylesheets significantly reduce memory usage and improve performance as the number of components increases. This technique is especially valuable in applications like YouTube, where dozens or hundreds of component instances might appear on a single page.</p>\n<h2 id=\"accessibility-in-production-web-components\">Accessibility in Production Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#accessibility-in-production-web-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>Successful Web Components in production prioritize accessibility:</p>\n<pre><code class=\"language-javascript\">class AccessibleTabPanel extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.shadowRoot.innerHTML = `\n      &lt;div class=\"tabs\" role=\"tablist\"&gt;\n        &lt;slot name=\"tab\"&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n      &lt;div class=\"panels\"&gt;\n        &lt;slot name=\"panel\"&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add keyboard navigation, ARIA attributes, etc.\n  }\n\n  connectedCallback() {\n    // Set up accessibility requirements\n    const tabs = this.querySelectorAll('[slot=\"tab\"]');\n    const panels = this.querySelectorAll('[slot=\"panel\"]');\n\n    tabs.forEach((tab, index) =&gt; {\n      // Set ARIA attributes\n      tab.setAttribute('role', 'tab');\n      tab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');\n      tab.setAttribute('tabindex', index === 0 ? '0' : '-1');\n      tab.setAttribute('aria-controls', `panel-${index}`);\n\n      // Set panel attributes\n      panels[index].setAttribute('role', 'tabpanel');\n      panels[index].setAttribute('id', `panel-${index}`);\n      panels[index].setAttribute('aria-labelledby', tab.id);\n      panels[index].hidden = index !== 0;\n\n      // Add event listeners for keyboard navigation\n      // ...\n    });\n  }\n}\n</code></pre>\n<p>Published design systems such as Spectrum Web Components document accessibility work like this; read their docs for the details.</p>\n<p>Accessibility is a core part of web components. Let's explore an accessible tab panel component that demonstrates proper keyboard navigation and ARIA attributes:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Accessible Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n  &lt;h2&gt;Accessible Tab Panel Component Demo&lt;/h2&gt;\n  \n  &lt;div class=&quot;intro-box&quot;&gt;\n    &lt;h3&gt;Accessibility Features&lt;/h3&gt;\n    &lt;ul&gt;\n      &lt;li&gt;Proper ARIA roles, states, and properties&lt;/li&gt;\n      &lt;li&gt;Keyboard navigation between tabs&lt;/li&gt;\n      &lt;li&gt;Focus management and visible focus states&lt;/li&gt;\n      &lt;li&gt;Screen reader announcements&lt;/li&gt;\n    &lt;/ul&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;keyboard-instructions&quot;&gt;\n    &lt;h3&gt;Keyboard Navigation&lt;/h3&gt;\n    &lt;ul&gt;\n      &lt;li&gt;&lt;kbd&gt;Tab&lt;/kbd&gt; to focus on the tab list&lt;/li&gt;\n      &lt;li&gt;&lt;kbd&gt;→&lt;/kbd&gt; / &lt;kbd&gt;←&lt;/kbd&gt; to navigate between tabs&lt;/li&gt;\n      &lt;li&gt;&lt;kbd&gt;Home&lt;/kbd&gt; to move to the first tab&lt;/li&gt;\n      &lt;li&gt;&lt;kbd&gt;End&lt;/kbd&gt; to move to the last tab&lt;/li&gt;\n    &lt;/ul&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;component-container&quot;&gt;\n    &lt;accessible-tab-panel id=&quot;demo-tabs&quot;&gt;\n      &lt;div slot=&quot;tab&quot;&gt;HTML&lt;/div&gt;\n      &lt;div slot=&quot;panel&quot;&gt;\n        &lt;h3&gt;HTML&lt;/h3&gt;\n        &lt;p&gt;HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.&lt;/p&gt;\n        &lt;p&gt;It defines the meaning and structure of web content. HTML elements are the building blocks of HTML pages. With HTML constructs, images and other objects such as interactive forms may be embedded into the rendered page.&lt;/p&gt;\n      &lt;/div&gt;\n      \n      &lt;div slot=&quot;tab&quot;&gt;CSS&lt;/div&gt;\n      &lt;div slot=&quot;panel&quot;&gt;\n        &lt;h3&gt;CSS&lt;/h3&gt;\n        &lt;p&gt;CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in HTML or XML.&lt;/p&gt;\n        &lt;p&gt;CSS is designed to enable the separation of presentation and content, including layout, colors, and fonts. This separation improves content accessibility, provides more flexibility and control over presentation characteristics.&lt;/p&gt;\n      &lt;/div&gt;\n      \n      &lt;div slot=&quot;tab&quot;&gt;JavaScript&lt;/div&gt;\n      &lt;div slot=&quot;panel&quot;&gt;\n        &lt;h3&gt;JavaScript&lt;/h3&gt;\n        &lt;p&gt;JavaScript is a programming language that enables interactive web pages and is an essential part of web applications.&lt;/p&gt;\n        &lt;p&gt;It is used for client-side scripting to make web pages dynamic and interactive. JavaScript is also used in server-side programming, game development, and the creation of desktop and mobile applications.&lt;/p&gt;\n      &lt;/div&gt;\n      \n      &lt;div slot=&quot;tab&quot;&gt;Web Components&lt;/div&gt;\n      &lt;div slot=&quot;panel&quot;&gt;\n        &lt;h3&gt;Web Components&lt;/h3&gt;\n        &lt;p&gt;Web Components are a set of standards that allow developers to create custom, reusable HTML elements with encapsulated functionality.&lt;/p&gt;\n        &lt;p&gt;They consist of Custom Elements, Shadow DOM, and HTML Templates. Web Components enable component-based development in the browser without requiring external libraries or frameworks.&lt;/p&gt;\n      &lt;/div&gt;\n    &lt;/accessible-tab-panel&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;live-region&quot; aria-live=&quot;polite&quot; id=&quot;a11y-announcer&quot;&gt;&lt;/div&gt;\n  \n  &lt;div class=&quot;screen-reader-info&quot;&gt;\n    &lt;h3&gt;Screen Reader Output&lt;/h3&gt;\n    &lt;p&gt;A screen reader would announce the following for this component:&lt;/p&gt;\n    &lt;div class=&quot;reader-output&quot; id=&quot;reader-output&quot;&gt;\n      &lt;div class=&quot;output-entry&quot;&gt;Tab list with 4 tabs&lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  --color-primary: #4a6cf7;\n  --color-primary-hover: #3a5cf7;\n  --color-primary-light: #e0e7ff;\n  --color-text: #1f2937;\n  --color-text-light: #6b7280;\n  --color-border: #e5e7eb;\n  --color-surface: #ffffff;\n  --color-background: #f9fafb;\n  --color-focus: #3b82f6;\n  \n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n  --border-radius: 6px;\n  --focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.5);\n  \n  --transition-fast: 150ms ease;\n}\n\n* {\n  box-sizing: border-box;\n}\n\nbody {\n  font-family: var(--font-family);\n  line-height: 1.5;\n  color: var(--color-text);\n  background-color: var(--color-background);\n  margin: 0;\n  padding: 20px;\n}\n\n.container {\n  max-width: 800px;\n  margin: 0 auto;\n}\n\nh2 {\n  font-size: 24px;\n  margin-top: 0;\n  margin-bottom: 24px;\n}\n\nh3 {\n  font-size: 18px;\n  margin-top: 0;\n  margin-bottom: 12px;\n}\n\n.intro-box {\n  background-color: #e0f2fe;\n  border-left: 4px solid var(--color-focus);\n  padding: 16px;\n  margin-bottom: 20px;\n  border-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n\n.intro-box ul {\n  margin: 0;\n  padding-left: 24px;\n}\n\n.keyboard-instructions {\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  margin-bottom: 20px;\n  box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n\n.keyboard-instructions ul {\n  margin: 0;\n  padding-left: 24px;\n}\n\nkbd {\n  background-color: #f1f5f9;\n  border: 1px solid #cbd5e1;\n  border-radius: 3px;\n  box-shadow: 0 1px 1px rgba(0,0,0,0.2);\n  padding: 2px 6px;\n  font-size: 12px;\n  font-family: monospace;\n}\n\n.component-container {\n  margin-bottom: 30px;\n  border: 1px dashed #cbd5e1;\n  padding: 20px;\n  border-radius: var(--border-radius);\n}\n\n.live-region {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.screen-reader-info {\n  background-color: var(--color-surface);\n  padding: 16px;\n  border-radius: var(--border-radius);\n  margin-top: 20px;\n  box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n\n.reader-output {\n  background-color: #f1f5f9;\n  border: 1px solid #cbd5e1;\n  border-radius: 4px;\n  padding: 12px;\n  font-family: monospace;\n  max-height: 200px;\n  overflow-y: auto;\n}\n\n.output-entry {\n  margin-bottom: 8px;\n  padding-bottom: 8px;\n  border-bottom: 1px solid #e2e8f0;\n}\n\n.output-entry:last-child {\n  margin-bottom: 0;\n  padding-bottom: 0;\n  border-bottom: 0;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Helper function to simulate screen reader announcements\nfunction announceToScreenReader(message) {\n  // Announce to actual screen readers via aria-live\n  const liveRegion = document.getElementById(&#39;a11y-announcer&#39;);\n  if (liveRegion) {\n    liveRegion.textContent = message;\n  }\n\n  // Also show in our reader output visualization\n  const readerOutput = document.getElementById(&#39;reader-output&#39;);\n  if (readerOutput) {\n    const entry = document.createElement(&#39;div&#39;);\n    entry.className = &#39;output-entry&#39;;\n    entry.textContent = message;\n    readerOutput.appendChild(entry);\n\n    // Auto-scroll to bottom\n    readerOutput.scrollTop = readerOutput.scrollHeight;\n  }\n}\n\n// Define the AccessibleTabPanel component\nclass AccessibleTabPanel extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Component state\n    this._tabs = [];\n    this._panels = [];\n    this._selectedIndex = 0;\n\n    // Initial render\n    this.render();\n  }\n\n  connectedCallback() {\n    // Process slots when component is connected to DOM\n    this.shadowRoot.addEventListener(&#39;slotchange&#39;, this._processSlots.bind(this));\n\n    // Initial processing if slots are already populated\n    this._processSlots();\n\n    // Announce to screen reader that component is ready\n    announceToScreenReader(&#39;Tab panel component loaded with 4 tabs&#39;);\n  }\n\n  // Process tab and panel slots\n  _processSlots() {\n    const tabSlot = this.shadowRoot.querySelector(&#39;slot[name=&quot;tab&quot;]&#39;);\n    const panelSlot = this.shadowRoot.querySelector(&#39;slot[name=&quot;panel&quot;]&#39;);\n\n    if (tabSlot &amp;&amp; panelSlot) {\n      // Get assigned elements\n      this._tabs = tabSlot.assignedElements();\n      this._panels = panelSlot.assignedElements();\n\n      // Set up tabs and panels\n      if (this._tabs.length &gt; 0 &amp;&amp; this._panels.length &gt; 0) {\n        this._setupTabsAndPanels();\n        this._selectTab(this._selectedIndex);\n      }\n    }\n  }\n\n  // Set up tabs and panels with proper attributes and event listeners\n  _setupTabsAndPanels() {\n    const tablist = this.shadowRoot.querySelector(&#39;[role=&quot;tablist&quot;]&#39;);\n\n    // Configure tabs\n    this._tabs.forEach((tab, index) =&gt; {\n      // Set tab attributes\n      tab.setAttribute(&#39;role&#39;, &#39;tab&#39;);\n      tab.setAttribute(&#39;id&#39;, `tab-${this.id}-${index}`);\n      tab.setAttribute(&#39;aria-selected&#39;, index === this._selectedIndex ? &#39;true&#39; : &#39;false&#39;);\n      tab.setAttribute(&#39;tabindex&#39;, index === this._selectedIndex ? &#39;0&#39; : &#39;-1&#39;);\n      tab.setAttribute(&#39;aria-controls&#39;, `panel-${this.id}-${index}`);\n\n      // Add click event listener\n      tab.addEventListener(&#39;click&#39;, () =&gt; this._selectTab(index));\n\n      // Add keydown event listener for keyboard navigation\n      tab.addEventListener(&#39;keydown&#39;, e =&gt; this._handleTabKeyDown(e, index));\n    });\n\n    // Configure panels\n    this._panels.forEach((panel, index) =&gt; {\n      // Set panel attributes\n      panel.setAttribute(&#39;role&#39;, &#39;tabpanel&#39;);\n      panel.setAttribute(&#39;id&#39;, `panel-${this.id}-${index}`);\n      panel.setAttribute(&#39;aria-labelledby&#39;, `tab-${this.id}-${index}`);\n      panel.setAttribute(&#39;tabindex&#39;, &#39;0&#39;); // Make panel focusable\n      panel.hidden = index !== this._selectedIndex;\n    });\n\n    // Add keydown event listener to tablist for keyboard navigation\n    tablist.addEventListener(&#39;keydown&#39;, this._handleTablistKeyDown.bind(this));\n  }\n\n  // Handle keydown events on the tablist\n  _handleTablistKeyDown(event) {\n    // This is a fallback for the whole tablist, individual tabs have their own handlers\n    if (event.key === &#39;ArrowRight&#39; || event.key === &#39;ArrowLeft&#39;) {\n      event.preventDefault(); // Prevent scrolling\n    }\n  }\n\n  // Handle keydown events on individual tabs\n  _handleTabKeyDown(event, index) {\n    let newIndex = index;\n\n    switch (event.key) {\n      case &#39;ArrowRight&#39;:\n        event.preventDefault();\n        newIndex = (index + 1) % this._tabs.length;\n        this._tabs[newIndex].focus();\n        this._selectTab(newIndex);\n        break;\n\n      case &#39;ArrowLeft&#39;:\n        event.preventDefault();\n        newIndex = (index - 1 + this._tabs.length) % this._tabs.length;\n        this._tabs[newIndex].focus();\n        this._selectTab(newIndex);\n        break;\n\n      case &#39;Home&#39;:\n        event.preventDefault();\n        this._tabs[0].focus();\n        this._selectTab(0);\n        break;\n\n      case &#39;End&#39;:\n        event.preventDefault();\n        newIndex = this._tabs.length - 1;\n        this._tabs[newIndex].focus();\n        this._selectTab(newIndex);\n        break;\n\n      case &#39;Enter&#39;:\n      case &#39; &#39;:\n        event.preventDefault();\n        this._selectTab(index);\n        break;}\n\n  }\n\n  // Select a tab and show its panel\n  _selectTab(index) {\n    if (index &lt; 0 || index &gt;= this._tabs.length) return;\n\n    // Update selected tab\n    this._tabs.forEach((tab, i) =&gt; {\n      const selected = i === index;\n      tab.setAttribute(&#39;aria-selected&#39;, selected ? &#39;true&#39; : &#39;false&#39;);\n      tab.setAttribute(&#39;tabindex&#39;, selected ? &#39;0&#39; : &#39;-1&#39;);\n      // Apply selected styling\n      tab.classList.toggle(&#39;selected&#39;, selected);\n    });\n\n    // Update visible panel\n    this._panels.forEach((panel, i) =&gt; {\n      panel.hidden = i !== index;\n    });\n\n    // Update state\n    this._selectedIndex = index;\n\n    // Announce tab change to screen reader\n    const tabName = this._tabs[index].textContent.trim();\n    announceToScreenReader(`Selected tab: ${tabName}`);\n  }\n\n  // Render the component\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .tabs-container {\n          display: flex;\n          flex-direction: column;\n        }\n        \n        .tablist {\n          display: flex;\n          border-bottom: 1px solid var(--color-border, #e5e7eb);\n          margin-bottom: 20px;\n        }\n        \n        ::slotted([slot=&quot;tab&quot;]) {\n          padding: 10px 20px;\n          border: 1px solid transparent;\n          border-bottom: none;\n          border-top-left-radius: 4px;\n          border-top-right-radius: 4px;\n          cursor: pointer;\n          margin-right: 4px;\n          margin-bottom: -1px;\n          background-color: var(--color-background, #f9fafb);\n          position: relative;\n          transition: all var(--transition-fast, 150ms ease);\n        }\n        \n        ::slotted([slot=&quot;tab&quot;]:hover) {\n          background-color: var(--color-primary-light, #e0e7ff);\n        }\n        \n        ::slotted([slot=&quot;tab&quot;].selected) {\n          background-color: var(--color-surface, #ffffff);\n          border-color: var(--color-border, #e5e7eb);\n          border-bottom-color: var(--color-surface, #ffffff);\n          color: var(--color-primary, #4a6cf7);\n          font-weight: 500;\n        }\n        \n        /* Focus styles */\n        ::slotted([slot=&quot;tab&quot;]:focus-visible) {\n          outline: none;\n          box-shadow: var(--focus-ring, 0 0 0 3px rgba(59, 130, 246, 0.5));\n          position: relative;\n          z-index: 1;\n        }\n        \n        /* Prevent focus ring AND selected styles overlapping */\n        ::slotted([slot=&quot;tab&quot;].selected:focus-visible) {\n          border-bottom-color: var(--color-primary-light, #e0e7ff);\n        }\n        \n        .panels-container {\n          min-height: 200px;\n          background-color: var(--color-surface, #ffffff);\n          border-radius: 0 0 var(--border-radius, 6px) var(--border-radius, 6px);\n          padding: 16px 0;\n        }\n        \n        ::slotted([slot=&quot;panel&quot;]) {\n          margin: 0;\n          padding: 0 16px;\n        }\n        \n        /* Focus styles for panels */\n        ::slotted([slot=&quot;panel&quot;]:focus-visible) {\n          outline: none;\n          box-shadow: 0 0 0 2px var(--color-focus, #3b82f6);\n          border-radius: var(--border-radius, 6px);\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;tabs-container&quot;&gt;\n        &lt;div class=&quot;tablist&quot; role=&quot;tablist&quot; aria-label=&quot;Programming topics&quot;&gt;\n          &lt;slot name=&quot;tab&quot;&gt;&lt;/slot&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;panels-container&quot;&gt;\n          &lt;slot name=&quot;panel&quot;&gt;&lt;/slot&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;accessible-tab-panel&#39;, AccessibleTabPanel);\n\n// Initialize the demo\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  // Set up demonstration features\n  const tabPanel = document.getElementById(&#39;demo-tabs&#39;);\n\n  // Ensure the component is initialized\n  if (tabPanel) {\n    // Listen for tab changes for demo purposes\n    tabPanel.addEventListener(&#39;keydown&#39;, e =&gt; {\n      if ([&#39;ArrowRight&#39;, &#39;ArrowLeft&#39;, &#39;Home&#39;, &#39;End&#39;].includes(e.key)) {\n        announceToScreenReader(`Navigated with ${e.key} key`);\n      }\n    });\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This accessible tab panel implementation showcases several key accessibility features:</p>\n<ol>\n<li>\n<p><strong>Proper ARIA roles and attributes</strong> - Using <code>role=\"tablist\"</code>, <code>role=\"tab\"</code>, and <code>role=\"tabpanel\"</code> with appropriate relationships</p>\n</li>\n<li>\n<p><strong>Keyboard navigation</strong> - Arrow keys to move between tabs, Home/End to jump to first/last tab</p>\n</li>\n<li>\n<p><strong>Focus management</strong> - Visible focus indicators and proper focus order</p>\n</li>\n<li>\n<p><strong>Screen reader support</strong> - Appropriate announcements when tabs change</p>\n</li>\n</ol>\n<p>Try navigating the tabs using only your keyboard. Press Tab to focus on the tab list, then use arrow keys to move between tabs. Notice how the focus is visually indicated and how the selected tab's content is displayed.</p>\n<p>This implementation follows the <a href=\"https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/\">WAI-ARIA Authoring Practices</a> <a href=\"https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/\">for tab panels,</a> ensuring that users of assistive technologies can effectively interact with the component.</p>\n<h2 id=\"integration-with-frameworks\">Integration with Frameworks<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#integration-with-frameworks\" aria-label=\"Link to this section\">#</a></h2>\n<p>Many organizations need to integrate Web Components with existing framework codebases:</p>\n<h3 id=\"react-integration\">React Integration<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#react-integration\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-jsx\">// React wrapper around a Web Component\nimport React, { useRef, useEffect } from 'react';\nimport 'my-web-components/dist/data-chart.js';\n\nfunction DataChartWrapper(props) {\n  const chartRef = useRef(null);\n\n  useEffect(() =&gt; {\n    // Pass complex data as a property\n    chartRef.current.data = props.data;\n  }, [props.data]);\n\n  useEffect(() =&gt; {\n    // Custom events: attach the listener yourself\n    const el = chartRef.current;\n    const handler = (e) =&gt; props.onUpdate?.(e.detail);\n    el.addEventListener('chart-update', handler);\n    return () =&gt; el.removeEventListener('chart-update', handler);\n  }, [props.onUpdate]);\n\n  // Simple values go in as attributes\n  return &lt;data-chart ref={chartRef} theme={props.theme} height={props.height} /&gt;;\n}\n</code></pre>\n<p>Note the listener: writing <code>onChartUpdate={props.onUpdate}</code> on the element does not attach a listener for a custom event in React 18 and earlier, and React 19 is not covered here. <code>addEventListener</code> on the ref works in every version.</p>\n<p>To see how React and Web Components work together in practice, here's an interactive demo showing the integration patterns in action:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Framework Integration Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;react-app&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  --color-primary: #4a6cf7;\n  --color-primary-light: #e0e7ff;\n  --color-surface: #ffffff;\n  --color-background: #f9fafb;\n  --color-border: #e5e7eb;\n  --color-text: #1f2937;\n  --color-text-light: #6b7280;\n  --color-success: #10b981;\n  --color-warning: #f59e0b;\n  \n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n  --border-radius: 6px;\n  --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n}\n\nbody {\n  font-family: var(--font-family);\n  margin: 0;\n  padding: 16px;\n  background-color: var(--color-background);\n  color: var(--color-text);\n}\n\n.app-container {\n  max-width: 800px;\n  margin: 0 auto;\n}\n\n.header {\n  margin-bottom: 24px;\n}\n\nh1 {\n  font-size: 24px;\n  margin-top: 0;\n  margin-bottom: 8px;\n}\n\n.intro {\n  background-color: var(--color-primary-light);\n  border-left: 4px solid var(--color-primary);\n  padding: 16px;\n  margin-bottom: 24px;\n  border-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n\n.integration-demo {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 24px;\n  margin-bottom: 24px;\n}\n\n@media (max-width: 700px) {\n  .integration-demo {\n    grid-template-columns: 1fr;\n  }\n}\n\n.react-side, .webcomponent-side {\n  background-color: var(--color-surface);\n  border-radius: var(--border-radius);\n  box-shadow: var(--shadow-sm);\n  padding: 20px;\n}\n\n.side-label {\n  display: inline-block;\n  background-color: #f1f5f9;\n  color: var(--color-text-light);\n  font-size: 12px;\n  padding: 4px 8px;\n  border-radius: 4px;\n  margin-bottom: 16px;\n}\n\n.section-title {\n  font-size: 18px;\n  margin-top: 0;\n  margin-bottom: 16px;\n  font-weight: 600;\n}\n\n.control-group {\n  margin-bottom: 16px;\n}\n\n.control-group label {\n  display: block;\n  margin-bottom: 8px;\n  font-weight: 500;\n}\n\n.color-options {\n  display: flex;\n  gap: 8px;\n  margin-bottom: 16px;\n}\n\n.color-option {\n  width: 32px;\n  height: 32px;\n  border-radius: 4px;\n  cursor: pointer;\n  position: relative;\n  transition: transform 0.2s;\n}\n\n.color-option:hover {\n  transform: scale(1.1);\n}\n\n.color-option.selected::after {\n  content: &quot;✓&quot;;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  color: white;\n  text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);\n}\n\nbutton {\n  background-color: var(--color-primary);\n  color: white;\n  border: none;\n  padding: 8px 16px;\n  border-radius: 4px;\n  font-family: inherit;\n  font-size: 14px;\n  cursor: pointer;\n  transition: background-color 0.2s;\n}\n\nbutton:hover {\n  background-color: #3a5cf7;\n}\n\n.counter-display {\n  font-size: 24px;\n  font-weight: 600;\n  margin-bottom: 16px;\n  color: var(--color-primary);\n}\n\n.message-display {\n  margin-top: 16px;\n  margin-bottom: 16px;\n  padding: 12px;\n  background-color: #f1f5f9;\n  border-radius: 4px;\n  font-family: monospace;\n}\n\n.event-flow {\n  position: relative;\n  height: 120px;\n  margin-top: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.event-arrow {\n  width: 80%;\n  height: 2px;\n  background-color: var(--color-text-light);\n  position: relative;\n  margin: 0 auto;\n}\n\n.event-arrow::before, .event-arrow::after {\n  content: &quot;&quot;;\n  position: absolute;\n  top: -4px;\n  width: 10px;\n  height: 10px;\n  border-radius: 50%;\n  background-color: var(--color-text-light);\n}\n\n.event-arrow::before {\n  left: 0;\n}\n\n.event-arrow::after {\n  right: 0;\n}\n\n.event-label {\n  position: absolute;\n  top: 10px;\n  left: 50%;\n  transform: translateX(-50%);\n  background-color: var(--color-surface);\n  padding: 4px 8px;\n  font-size: 12px;\n  border-radius: 4px;\n  box-shadow: var(--shadow-sm);\n  white-space: nowrap;\n}\n\n.event-trigger .event-label {\n  top: auto;\n  bottom: 10px;\n}\n\n.event-pulse {\n  position: absolute;\n  top: -4px;\n  left: 50%;\n  transform: translateX(-50%);\n  width: 10px;\n  height: 10px;\n  background-color: var(--color-success);\n  border-radius: 50%;\n}\n\n.pulse-animation {\n  animation: pulse 1s linear;\n}\n\n@keyframes pulse {\n  0% {\n    box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);\n  }\n  70% {\n    box-shadow: 0 0 0 15px rgba(16, 185, 129, 0);\n  }\n  100% {\n    box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);\n  }\n}\n\n.code-section {\n  background-color: var(--color-surface);\n  border-radius: var(--border-radius);\n  padding: 20px;\n  margin-bottom: 24px;\n  box-shadow: var(--shadow-sm);\n}\n\n.code-tabs {\n  display: flex;\n  border-bottom: 1px solid var(--color-border);\n  margin-bottom: 16px;\n}\n\n.code-tab {\n  padding: 8px 16px;\n  cursor: pointer;\n  position: relative;\n  user-select: none;\n}\n\n.code-tab.active {\n  font-weight: 600;\n  color: var(--color-primary);\n}\n\n.code-tab.active::after {\n  content: &quot;&quot;;\n  position: absolute;\n  bottom: -1px;\n  left: 0;\n  right: 0;\n  height: 2px;\n  background-color: var(--color-primary);\n}\n\n.code-content {\n  position: relative;\n}\n\npre {\n  background-color: #f8fafc;\n  padding: 16px;\n  border-radius: 4px;\n  overflow-x: auto;\n  margin: 0;\n}\n\ncode {\n  font-family: monospace;\n  font-size: 14px;\n  line-height: 1.5;\n}\n\n/* Counter component custom styles */\ncounter-element {\n  display: block;\n  border-radius: var(--border-radius);\n  padding: 20px;\n  margin-bottom: 16px;\n  transition: all 0.3s ease;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class CounterElement extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initialize properties with default values\n    this._count = 0;\n    this._color = &#39;#4a6cf7&#39;;\n\n    // Render initial state\n    this.render();\n  }\n\n  // Define which attributes to observe\n  static get observedAttributes() {\n    return [&#39;count&#39;, &#39;color&#39;];\n  }\n\n  // Handle attribute changes\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (oldValue === newValue) return;\n\n    if (name === &#39;count&#39;) {\n      this._count = parseInt(newValue, 10);\n    } else if (name === &#39;color&#39;) {\n      this._color = newValue;\n    }\n\n    this.render();\n  }\n\n  // Getter/setter for count property\n  get count() {\n    return this._count;\n  }\n\n  set count(value) {\n    this._count = value;\n    this.setAttribute(&#39;count&#39;, value);\n  }\n\n  // Getter/setter for color property\n  get color() {\n    return this._color;\n  }\n\n  set color(value) {\n    this._color = value;\n    this.setAttribute(&#39;color&#39;, value);\n  }\n\n  // Handle click event\n  handleIncrement() {\n    this._count++;\n    this.setAttribute(&#39;count&#39;, this._count);\n\n    // Dispatch custom event\n    this.dispatchEvent(new CustomEvent(&#39;counter-incremented&#39;, {\n      bubbles: true,\n      composed: true,\n      detail: { count: this._count } }));\n\n  }\n\n  // Render the component\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          font-family: var(--font-family, system-ui, sans-serif);\n        }\n        \n        .counter-container {\n          background-color: ${this._color}15;\n          border: 2px solid ${this._color};\n          border-radius: 8px;\n          padding: 16px;\n          text-align: center;\n          transition: all 0.3s ease;\n        }\n        \n        .counter-value {\n          font-size: 32px;\n          font-weight: bold;\n          color: ${this._color};\n          margin-bottom: 16px;\n        }\n        \n        button {\n          background-color: ${this._color};\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 16px;\n          font-size: 16px;\n          cursor: pointer;\n          transition: background-color 0.2s;\n        }\n        \n        button:hover {\n          filter: brightness(110%);\n        }\n        \n        .counter-label {\n          margin-top: 8px;\n          font-size: 14px;\n          color: #6b7280;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;counter-container&quot;&gt;\n        &lt;div class=&quot;counter-value&quot;&gt;${this._count}&lt;/div&gt;\n        &lt;button id=&quot;increment-btn&quot;&gt;Increment from Web Component&lt;/button&gt;\n        &lt;div class=&quot;counter-label&quot;&gt;Web Component Internal Counter&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listener to button\n    this.shadowRoot.getElementById(&#39;increment-btn&#39;).addEventListener(&#39;click&#39;, this.handleIncrement.bind(this));\n  }\n\n  // Clean up when element is removed\n  disconnectedCallback() {var _this$shadowRoot$getE;\n    // Remove event listeners if needed\n    (_this$shadowRoot$getE = this.shadowRoot.getElementById(&#39;increment-btn&#39;)) === null || _this$shadowRoot$getE === void 0 ? void 0 : _this$shadowRoot$getE.removeEventListener(&#39;click&#39;, this.handleIncrement);\n  }}\n\n\n// Register custom element\ncustomElements.define(&#39;counter-element&#39;, CounterElement);\n\n// Load React and ReactDOM from CDN\nfunction loadReactDependencies() {\n  return new Promise((resolve, reject) =&gt; {\n    // Load React\n    const reactScript = document.createElement(&#39;script&#39;);\n    reactScript.src = &#39;https://unpkg.com/react@18/umd/react.production.min.js&#39;;\n    reactScript.crossOrigin = true;\n    document.head.appendChild(reactScript);\n\n    // Load ReactDOM\n    const reactDOMScript = document.createElement(&#39;script&#39;);\n    reactDOMScript.src = &#39;https://unpkg.com/react-dom@18/umd/react-dom.production.min.js&#39;;\n    reactDOMScript.crossOrigin = true;\n    reactDOMScript.onload = resolve;\n    reactDOMScript.onerror = reject;\n    document.head.appendChild(reactDOMScript);\n  });\n}\n\n// Load Babel for JSX support\nfunction loadBabel() {\n  return new Promise((resolve, reject) =&gt; {\n    const babelScript = document.createElement(&#39;script&#39;);\n    babelScript.src = &#39;https://unpkg.com/@babel/standalone/babel.min.js&#39;;\n    babelScript.onload = resolve;\n    babelScript.onerror = reject;\n    document.head.appendChild(babelScript);\n  });\n}\n\n// Load dependencies and then initialize app\nasync function initApp() {\n  // Create and show loader\n  const loader = document.createElement(&#39;div&#39;);\n  loader.id = &#39;app-loader&#39;;\n  loader.innerHTML = `\n    &lt;style&gt;\n      #app-loader {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        display: flex;\n        flex-direction: column;\n        justify-content: center;\n        align-items: center;\n        background-color: rgba(255, 255, 255, 0.9);\n        z-index: 1000;\n        font-family: system-ui, sans-serif;\n      }\n      .spinner {\n        width: 50px;\n        height: 50px;\n        border: 5px solid #f3f3f3;\n        border-top: 5px solid #4a6cf7;\n        border-radius: 50%;\n        animation: spin 1s linear infinite;\n        margin-bottom: 20px;\n      }\n      .loading-text {\n        color: #333;\n        font-size: 18px;\n      }\n      @keyframes spin {\n        0% { transform: rotate(0deg); }\n        100% { transform: rotate(360deg); }\n      }\n    &lt;/style&gt;\n    &lt;div class=&quot;spinner&quot;&gt;&lt;/div&gt;\n    &lt;div class=&quot;loading-text&quot;&gt;Loading application...&lt;/div&gt;\n  `;\n  document.body.appendChild(loader);\n\n  try {\n    // React is provided by the sandbox (libs=react); the JSX below is already compiled.\n    // Add a delay to make sure everything is loaded\n    setTimeout(() =&gt; {\n      // Remove loader when rendering is complete\n      document.body.removeChild(loader);\n      renderReactApp();\n    }, 300);\n  } catch (error) {\n    console.error(&#39;Failed to load dependencies:&#39;, error);\n    // Update loader to show error\n    loader.innerHTML = `\n      &lt;style&gt;\n        #app-loader {\n          position: fixed;\n          top: 0;\n          left: 0;\n          width: 100%;\n          height: 100%;\n          display: flex;\n          flex-direction: column;\n          justify-content: center;\n          align-items: center;\n          background-color: rgba(255, 255, 255, 0.9);\n          z-index: 1000;\n          font-family: system-ui, sans-serif;\n        }\n        .error-icon {\n          font-size: 50px;\n          color: #ef4444;\n          margin-bottom: 20px;\n        }\n        .error-text {\n          color: #333;\n          font-size: 18px;\n          max-width: 80%;\n          text-align: center;\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;error-icon&quot;&gt;❌&lt;/div&gt;\n      &lt;div class=&quot;error-text&quot;&gt;Failed to load application. Please refresh the page or try again later.&lt;/div&gt;\n    `;\n  }\n}\n\n// Render React app\nfunction renderReactApp() {\n  // Define the React component\n  const App = () =&gt; {var _colors$find;\n    const [count, setCount] = React.useState(0);\n    const [color, setColor] = React.useState(&#39;#4a6cf7&#39;);\n    const [lastEvent, setLastEvent] = React.useState(null);\n    const counterRef = React.useRef(null);\n    const [showPulse, setShowPulse] = React.useState(false);\n\n    // Available colors\n    const colors = [\n    { value: &#39;#4a6cf7&#39;, name: &#39;Blue&#39; },\n    { value: &#39;#10b981&#39;, name: &#39;Green&#39; },\n    { value: &#39;#f59e0b&#39;, name: &#39;Amber&#39; },\n    { value: &#39;#ef4444&#39;, name: &#39;Red&#39; },\n    { value: &#39;#8b5cf6&#39;, name: &#39;Purple&#39; }];\n\n\n    // Handle color change\n    const handleColorChange = newColor =&gt; {\n      setColor(newColor);\n    };\n\n    // Handle increment from React\n    const handleIncrement = () =&gt; {\n      setCount(prevCount =&gt; prevCount + 1);\n    };\n\n    // Handle event from web component\n    const handleCounterIncremented = event =&gt; {\n      const newCount = event.detail.count;\n      setCount(newCount);\n      setLastEvent(`counter-incremented event received: count=${newCount}`);\n\n      // Show pulse animation\n      setShowPulse(true);\n      setTimeout(() =&gt; setShowPulse(false), 1000);\n    };\n\n    // Effect to set properties on web component when React state changes\n    React.useEffect(() =&gt; {\n      if (counterRef.current) {\n        // Set properties directly\n        counterRef.current.count = count;\n        counterRef.current.color = color;\n      }\n    }, [count, color]);\n\n    // Effect to add event listeners on component mount\n    React.useEffect(() =&gt; {\n      const counter = counterRef.current;\n      if (counter) {\n        counter.addEventListener(&#39;counter-incremented&#39;, handleCounterIncremented);\n\n        // Cleanup\n        return () =&gt; {\n          counter.removeEventListener(&#39;counter-incremented&#39;, handleCounterIncremented);\n        };\n      }\n    }, []);\n\n    // Get selected color name\n    const selectedColorName = ((_colors$find = colors.find(c =&gt; c.value === color)) === null || _colors$find === void 0 ? void 0 : _colors$find.name) || &#39;Blue&#39;;\n\n    return /*#__PURE__*/(\n      React.createElement(&quot;div&quot;, { className: &quot;app-container&quot; }, /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;header&quot; }, /*#__PURE__*/\n      React.createElement(&quot;h1&quot;, null, &quot;React + Web Components Integration&quot;)), /*#__PURE__*/\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;intro&quot; }, /*#__PURE__*/\n      React.createElement(&quot;p&quot;, null, &quot;This demo shows how React can interact with Web Components. The React app sets properties on the Web Component through direct property access, and listens for events coming from the Web Component.&quot;)), /*#__PURE__*/\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;integration-demo&quot; }, /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;react-side&quot; }, /*#__PURE__*/\n      React.createElement(&quot;span&quot;, { className: &quot;side-label&quot; }, &quot;React App&quot;), /*#__PURE__*/\n      React.createElement(&quot;h2&quot;, { className: &quot;section-title&quot; }, &quot;React Controls&quot;), /*#__PURE__*/\n\n      React.createElement(&quot;div&quot;, { className: &quot;control-group&quot; }, /*#__PURE__*/\n      React.createElement(&quot;label&quot;, null, &quot;Color Theme:&quot;), /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;color-options&quot; },\n      colors.map((colorObj) =&gt; /*#__PURE__*/\n      React.createElement(&quot;div&quot;, {\n        key: colorObj.value,\n        className: `color-option ${color === colorObj.value ? &#39;selected&#39; : &#39;&#39;}`,\n        style: { backgroundColor: colorObj.value },\n        title: colorObj.name,\n        onClick: () =&gt; handleColorChange(colorObj.value) })))), /*#__PURE__*/\n\n\n\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;counter-display&quot; }, &quot;Count: &quot;,\n      count), /*#__PURE__*/\n\n\n      React.createElement(&quot;button&quot;, { onClick: handleIncrement }, &quot;Increment from React&quot;),\n\n\n\n      lastEvent &amp;&amp; /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;message-display&quot; },\n      lastEvent)), /*#__PURE__*/\n\n\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;webcomponent-side&quot; }, /*#__PURE__*/\n      React.createElement(&quot;span&quot;, { className: &quot;side-label&quot; }, &quot;Web Component&quot;), /*#__PURE__*/\n      React.createElement(&quot;h2&quot;, { className: &quot;section-title&quot; }, &quot;Counter Element&quot;), /*#__PURE__*/\n\n      React.createElement(&quot;counter-element&quot;, {\n        ref: counterRef,\n        count: count,\n        color: color,\n        style: { backgroundColor: `${color}15` } }), /*#__PURE__*/\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;component-info&quot; }, /*#__PURE__*/\n      React.createElement(&quot;div&quot;, null, /*#__PURE__*/React.createElement(&quot;strong&quot;, null, &quot;Current properties:&quot;)), /*#__PURE__*/\n      React.createElement(&quot;div&quot;, null, &quot;count: &quot;, count), /*#__PURE__*/\n      React.createElement(&quot;div&quot;, null, &quot;color: &quot;, selectedColorName)))), /*#__PURE__*/\n\n\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;event-flow&quot; }, /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;event-arrow&quot; }, /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: `event-pulse ${showPulse ? &#39;pulse-animation&#39; : &#39;&#39;}` })), /*#__PURE__*/\n\n      React.createElement(&quot;div&quot;, { className: &quot;event-label&quot; }, &quot;Web Component \\u2192 React&quot;), /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;event-label event-trigger&quot; }, &quot;React \\u2192 Web Component&quot;)), /*#__PURE__*/\n\n\n      React.createElement(&quot;div&quot;, { className: &quot;code-section&quot; }, /*#__PURE__*/\n      React.createElement(&quot;div&quot;, { className: &quot;section-title&quot; }, &quot;Integration Code&quot;), /*#__PURE__*/\n      React.createElement(&quot;pre&quot;, null, /*#__PURE__*/\n      React.createElement(&quot;code&quot;, null, `// React Component\nfunction CounterWrapper() {\n  const [count, setCount] = React.useState(0);\n  const counterRef = React.useRef(null);\n  \n  // Handle event from web component\n  const handleCounterIncremented = (event) =&gt; {\n    setCount(event.detail.count);\n  };\n  \n  // Effect to set up event listener\n  React.useEffect(() =&gt; {\n    const counter = counterRef.current;\n    if (counter) {\n      counter.addEventListener(\n        &#39;counter-incremented&#39;, \n        handleCounterIncremented\n      );\n      \n      return () =&gt; {\n        counter.removeEventListener(\n          &#39;counter-incremented&#39;, \n          handleCounterIncremented\n        );\n      };\n    }\n  }, []);\n  \n  // Update property when React state changes\n  React.useEffect(() =&gt; {\n    if (counterRef.current) {\n      counterRef.current.count = count;\n    }\n  }, [count]);\n  \n  return (\n    &lt;div&gt;\n      &lt;counter-element ref={counterRef}&gt;&lt;/counter-element&gt;\n      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;\n        Increment\n      &lt;/button&gt;\n    &lt;/div&gt;\n  );\n}`)))));\n\n\n\n\n  };\n\n  const container = document.getElementById(&#39;react-app&#39;);\n  ReactDOM.render( /*#__PURE__*/React.createElement(App, null), container);\n}\n\n// Initialize the application\ninitApp();\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This demo showcases the key integration patterns described earlier:</p>\n<ol>\n<li>\n<p><strong>Using refs to access Web Component properties</strong> - React uses a ref to directly set properties on the Web Component</p>\n</li>\n<li>\n<p><strong>Event-based communication</strong> - The Web Component dispatches custom events that React listens for</p>\n</li>\n<li>\n<p><strong>Bidirectional data flow</strong> - Changes in either React or the Web Component are synchronized</p>\n</li>\n</ol>\n<p>Try changing the color or clicking the buttons on either side to see how React and the Web Component communicate. Notice how property changes flow from React to the Web Component, while events flow from the Web Component back to React.</p>\n<h2 id=\"micro-frontend-architectures-with-web-components\">Micro-Frontend Architectures with Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#micro-frontend-architectures-with-web-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>Micro-frontends split one page into parts that separate teams build and ship. Web components are one way to give each part a boundary:</p>\n<pre><code class=\"language-html\">&lt;!-- Shell application --&gt;\n&lt;html&gt;\n&lt;head&gt;\n  &lt;title&gt;Banking Dashboard&lt;/title&gt;\n  &lt;!-- Import shared components --&gt;\n  &lt;script type=\"module\" src=\"https://mnixblog.netlify.app/shared/components/ing-header.js\"&gt;&lt;/script&gt;\n  &lt;script type=\"module\" src=\"https://mnixblog.netlify.app/shared/components/ing-footer.js\"&gt;&lt;/script&gt;\n&lt;/head&gt;\n&lt;body&gt;\n  &lt;!-- Shared header component owned by core team --&gt;\n  &lt;ing-header user-id=\"12345\"&gt;&lt;/ing-header&gt;\n\n  &lt;!-- Micro-frontend container --&gt;\n  &lt;main&gt;\n    &lt;!-- Accounts micro-frontend owned by accounts team --&gt;\n    &lt;accounts-overview&gt;&lt;/accounts-overview&gt;\n\n    &lt;!-- Investments micro-frontend owned by investments team --&gt;\n    &lt;investments-summary&gt;&lt;/investments-summary&gt;\n\n    &lt;!-- Loans micro-frontend owned by loans team --&gt;\n    &lt;loans-overview&gt;&lt;/loans-overview&gt;\n  &lt;/main&gt;\n\n  &lt;!-- Shared footer component --&gt;\n  &lt;ing-footer&gt;&lt;/ing-footer&gt;\n\n  &lt;!-- Load micro-frontends --&gt;\n  &lt;script type=\"module\" src=\"https://mnixblog.netlify.app/accounts/components/accounts-overview.js\"&gt;&lt;/script&gt;\n  &lt;script type=\"module\" src=\"https://mnixblog.netlify.app/investments/components/investments-summary.js\"&gt;&lt;/script&gt;\n  &lt;script type=\"module\" src=\"https://mnixblog.netlify.app/loans/components/loans-overview.js\"&gt;&lt;/script&gt;\n&lt;/body&gt;\n&lt;/html&gt;\n</code></pre>\n<p>This approach allows different teams to develop and deploy independently while maintaining a cohesive user experience.</p>\n<p>Let's visualize how a micro-frontend architecture might work in practice with this interactive demo:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Micro-Frontend Web Component Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;app-container&quot;&gt;\n  &lt;header class=&quot;shell-header core-team&quot;&gt;\n    &lt;div class=&quot;team-tag&quot;&gt;Core Team&lt;/div&gt;\n    &lt;div class=&quot;shell-logo&quot;&gt;Banking Portal&lt;/div&gt;\n    &lt;div class=&quot;user-info&quot;&gt;\n      &lt;span class=&quot;user-name&quot;&gt;Jane Doe&lt;/span&gt;\n      &lt;span class=&quot;user-avatar&quot;&gt;JD&lt;/span&gt;\n    &lt;/div&gt;\n  &lt;/header&gt;\n  \n  &lt;nav class=&quot;shell-nav core-team&quot;&gt;\n    &lt;div class=&quot;team-tag&quot;&gt;Core Team&lt;/div&gt;\n    &lt;ul class=&quot;nav-items&quot;&gt;\n      &lt;li class=&quot;nav-item active&quot; data-section=&quot;accounts&quot;&gt;Accounts&lt;/li&gt;\n      &lt;li class=&quot;nav-item&quot; data-section=&quot;investments&quot;&gt;Investments&lt;/li&gt;\n      &lt;li class=&quot;nav-item&quot; data-section=&quot;loans&quot;&gt;Loans&lt;/li&gt;\n      &lt;li class=&quot;nav-item&quot; data-section=&quot;support&quot;&gt;Support&lt;/li&gt;\n    &lt;/ul&gt;\n  &lt;/nav&gt;\n  \n  &lt;div class=&quot;content-area&quot;&gt;\n    &lt;section id=&quot;microfrontend-accounts&quot; class=&quot;accounts-team active&quot;&gt;\n      &lt;div class=&quot;team-tag&quot;&gt;Accounts Team&lt;/div&gt;\n      &lt;accounts-dashboard&gt;&lt;/accounts-dashboard&gt;\n    &lt;/section&gt;\n    \n    &lt;section id=&quot;microfrontend-investments&quot; class=&quot;investments-team&quot;&gt;\n      &lt;div class=&quot;team-tag&quot;&gt;Investments Team&lt;/div&gt;\n      &lt;investments-summary&gt;&lt;/investments-summary&gt;\n    &lt;/section&gt;\n    \n    &lt;section id=&quot;microfrontend-loans&quot; class=&quot;loans-team&quot;&gt;\n      &lt;div class=&quot;team-tag&quot;&gt;Loans Team&lt;/div&gt;\n      &lt;loans-overview&gt;&lt;/loans-overview&gt;\n    &lt;/section&gt;\n    \n    &lt;section id=&quot;microfrontend-support&quot; class=&quot;support-team&quot;&gt;\n      &lt;div class=&quot;team-tag&quot;&gt;Support Team&lt;/div&gt;\n      &lt;support-widget&gt;&lt;/support-widget&gt;\n    &lt;/section&gt;\n  &lt;/div&gt;\n  \n  &lt;footer class=&quot;shell-footer core-team&quot;&gt;\n    &lt;div class=&quot;team-tag&quot;&gt;Core Team&lt;/div&gt;\n    &lt;div class=&quot;footer-content&quot;&gt;\n      &lt;p&gt;© 2025 Demo Banking Portal | Privacy Policy | Terms of Service&lt;/p&gt;\n    &lt;/div&gt;\n  &lt;/footer&gt;\n  \n  &lt;div class=&quot;event-log&quot;&gt;\n    &lt;h3&gt;Cross-Team Communication Log&lt;/h3&gt;\n    &lt;div id=&quot;event-log-content&quot;&gt;&lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div class=&quot;architecture-diagram&quot;&gt;\n    &lt;h3&gt;Micro-Frontend Architecture&lt;/h3&gt;\n    &lt;div class=&quot;diagram&quot;&gt;\n      &lt;div class=&quot;diagram-shell&quot;&gt;\n        &lt;div class=&quot;diagram-label&quot;&gt;Shell Application (Core Team)&lt;/div&gt;\n        &lt;div class=&quot;diagram-container&quot;&gt;\n          &lt;div class=&quot;diagram-header&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;diagram-nav&quot;&gt;&lt;/div&gt;\n          &lt;div class=&quot;diagram-content&quot;&gt;\n            &lt;div class=&quot;diagram-mfe accounts&quot;&gt;&lt;/div&gt;\n            &lt;div class=&quot;diagram-mfe investments&quot;&gt;&lt;/div&gt;\n            &lt;div class=&quot;diagram-mfe loans&quot;&gt;&lt;/div&gt;\n            &lt;div class=&quot;diagram-mfe support&quot;&gt;&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;diagram-footer&quot;&gt;&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n  /* Core Team Colors */\n  --core-team-color: #3b82f6;\n  --core-team-light: #eff6ff;\n  \n  /* Accounts Team Colors */\n  --accounts-team-color: #10b981;\n  --accounts-team-light: #ecfdf5;\n  \n  /* Investments Team Colors */\n  --investments-team-color: #8b5cf6;\n  --investments-team-light: #f5f3ff;\n  \n  /* Loans Team Colors */\n  --loans-team-color: #f59e0b;\n  --loans-team-light: #fffbeb;\n  \n  /* Support Team Colors */\n  --support-team-color: #ef4444;\n  --support-team-light: #fef2f2;\n  \n  /* UI Colors */\n  --surface-color: #ffffff;\n  --border-color: #e5e7eb;\n  --text-color: #111827;\n  --text-secondary: #6b7280;\n  --background-color: #f9fafb;\n  \n  /* Dimensions */\n  --header-height: 60px;\n  --footer-height: 60px;\n  --nav-width: 200px;\n  \n  /* Font */\n  --font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nbody {\n  font-family: var(--font-family);\n  background-color: var(--background-color);\n  color: var(--text-color);\n  line-height: 1.5;\n  margin: 0;\n  padding: 0;\n}\n\n.app-container {\n  max-width: 1200px;\n  margin: 0 auto;\n  padding: 20px;\n}\n\n/* Team tags */\n.team-tag {\n  position: absolute;\n  top: 0;\n  right: 0;\n  font-size: 11px;\n  padding: 2px 6px;\n  border-radius: 0 0 0 4px;\n  color: white;\n  font-weight: 500;\n  opacity: 0.9;\n  z-index: 10;\n}\n\n.core-team .team-tag {\n  background-color: var(--core-team-color);\n}\n\n.accounts-team .team-tag {\n  background-color: var(--accounts-team-color);\n}\n\n.investments-team .team-tag {\n  background-color: var(--investments-team-color);\n}\n\n.loans-team .team-tag {\n  background-color: var(--loans-team-color);\n}\n\n.support-team .team-tag {\n  background-color: var(--support-team-color);\n}\n\n/* Shell Header */\n.shell-header {\n  position: relative;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 0 20px;\n  height: var(--header-height);\n  background-color: var(--surface-color);\n  border-bottom: 1px solid var(--border-color);\n  margin-bottom: 20px;\n  border-radius: 8px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n\n.shell-logo {\n  font-size: 20px;\n  font-weight: 600;\n  color: var(--core-team-color);\n}\n\n.user-info {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.user-name {\n  font-weight: 500;\n}\n\n.user-avatar {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 32px;\n  height: 32px;\n  background-color: var(--core-team-color);\n  color: white;\n  border-radius: 50%;\n  font-weight: 500;\n}\n\n/* Shell Navigation and Content Layout */\n.content-area {\n  display: flex;\n  margin-bottom: 20px;\n  min-height: 400px;\n}\n\n.shell-nav {\n  position: relative;\n  width: var(--nav-width);\n  background-color: var(--surface-color);\n  border-radius: 8px;\n  margin-right: 20px;\n  padding: 20px 0;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n  flex-shrink: 0;\n}\n\n.nav-items {\n  list-style: none;\n  margin-top: 20px;\n}\n\n.nav-item {\n  padding: 10px 20px;\n  margin-bottom: 5px;\n  cursor: pointer;\n  border-left: 3px solid transparent;\n  transition: all 0.2s ease;\n}\n\n.nav-item:hover {\n  background-color: var(--core-team-light);\n}\n\n.nav-item.active {\n  background-color: var(--core-team-light);\n  border-left-color: var(--core-team-color);\n  font-weight: 500;\n}\n\n/* Micro-frontend Sections */\n.content-area section {\n  position: relative;\n  flex: 1;\n  background-color: var(--surface-color);\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n  display: none;\n  min-height: 400px;\n}\n\n.content-area section.active {\n  display: block;\n}\n\n/* Shell Footer */\n.shell-footer {\n  position: relative;\n  height: var(--footer-height);\n  background-color: var(--surface-color);\n  border-radius: 8px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  text-align: center;\n  margin-bottom: 20px;\n}\n\n.footer-content {\n  color: var(--text-secondary);\n  font-size: 14px;\n}\n\n/* Event Log */\n.event-log {\n  background-color: var(--surface-color);\n  border-radius: 8px;\n  padding: 20px;\n  margin-bottom: 20px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n\n.event-log h3 {\n  margin-bottom: 10px;\n  font-size: 16px;\n  color: var(--text-color);\n}\n\n#event-log-content {\n  height: 150px;\n  overflow-y: auto;\n  background-color: #f8fafc;\n  border: 1px solid var(--border-color);\n  border-radius: 4px;\n  padding: 10px;\n  font-family: monospace;\n  font-size: 14px;\n}\n\n.log-entry {\n  margin-bottom: 8px;\n  padding-bottom: 8px;\n  border-bottom: 1px solid var(--border-color);\n}\n\n.log-entry:last-child {\n  margin-bottom: 0;\n  padding-bottom: 0;\n  border-bottom: none;\n}\n\n.log-time {\n  font-size: 12px;\n  color: var(--text-secondary);\n}\n\n.log-message {\n  margin-top: 2px;\n}\n\n/* Architecture Diagram */\n.architecture-diagram {\n  background-color: var(--surface-color);\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n\n.architecture-diagram h3 {\n  margin-bottom: 20px;\n  font-size: 16px;\n  color: var(--text-color);\n}\n\n.diagram {\n  display: flex;\n  justify-content: center;\n}\n\n.diagram-shell {\n  width: 80%;\n  max-width: 800px;\n}\n\n.diagram-label {\n  text-align: center;\n  margin-bottom: 10px;\n  font-weight: 500;\n  font-size: 14px;\n  color: var(--core-team-color);\n}\n\n.diagram-container {\n  border: 2px solid var(--core-team-color);\n  border-radius: 8px;\n  padding: 10px;\n  background-color: var(--core-team-light);\n}\n\n.diagram-header,\n.diagram-footer {\n  height: 30px;\n  background-color: rgba(59, 130, 246, 0.2);\n  border-radius: 4px;\n  margin-bottom: 10px;\n}\n\n.diagram-footer {\n  margin-bottom: 0;\n  margin-top: 10px;\n}\n\n.diagram-nav {\n  width: 20%;\n  height: 150px;\n  background-color: rgba(59, 130, 246, 0.2);\n  border-radius: 4px;\n  float: left;\n  margin-right: 10px;\n}\n\n.diagram-content {\n  overflow: hidden;\n  display: grid;\n  grid-template-columns: repeat(2, 1fr);\n  gap: 10px;\n  min-height: 150px;\n}\n\n.diagram-mfe {\n  border-radius: 4px;\n  padding: 15px;\n  font-size: 12px;\n  text-align: center;\n  font-weight: 500;\n  color: white;\n  position: relative;\n}\n\n.diagram-mfe::after {\n  position: absolute;\n  bottom: 5px;\n  left: 0;\n  right: 0;\n  text-align: center;\n  color: rgba(255, 255, 255, 0.8);\n}\n\n.diagram-mfe.accounts {\n  background-color: var(--accounts-team-color);\n}\n.diagram-mfe.accounts::after {\n  content: &quot;Accounts Team&quot;;\n}\n\n.diagram-mfe.investments {\n  background-color: var(--investments-team-color);\n}\n.diagram-mfe.investments::after {\n  content: &quot;Investments Team&quot;;\n}\n\n.diagram-mfe.loans {\n  background-color: var(--loans-team-color);\n}\n.diagram-mfe.loans::after {\n  content: &quot;Loans Team&quot;;\n}\n\n.diagram-mfe.support {\n  background-color: var(--support-team-color);\n}\n.diagram-mfe.support::after {\n  content: &quot;Support Team&quot;;\n}\n\n/* Responsive Adjustments */\n@media (max-width: 768px) {\n  .content-area {\n    flex-direction: column;\n  }\n  \n  .shell-nav {\n    width: 100%;\n    margin-right: 0;\n    margin-bottom: 20px;\n  }\n  \n  .diagram-nav {\n    width: 100%;\n    height: 40px;\n    float: none;\n    margin-right: 0;\n    margin-bottom: 10px;\n  }\n  \n  .diagram-content {\n    grid-template-columns: 1fr;\n  }\n}\n\n/* MFE Component Styles */\naccounts-dashboard {\n  display: block;\n  padding: 20px;\n  border: 2px solid var(--accounts-team-color);\n  border-radius: 8px;\n  background-color: var(--accounts-team-light);\n  min-height: 300px;\n}\n\ninvestments-summary {\n  display: block;\n  padding: 20px;\n  border: 2px solid var(--investments-team-color);\n  border-radius: 8px;\n  background-color: var(--investments-team-light);\n  min-height: 300px;\n}\n\nloans-overview {\n  display: block;\n  padding: 20px;\n  border: 2px solid var(--loans-team-color);\n  border-radius: 8px;\n  background-color: var(--loans-team-light);\n  min-height: 300px;\n}\n\nsupport-widget {\n  display: block;\n  padding: 20px;\n  border: 2px solid var(--support-team-color);\n  border-radius: 8px;\n  background-color: var(--support-team-light);\n  min-height: 300px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Log events to the event log\nfunction logEvent(source, action, data = null) {\n  const logContainer = document.getElementById(&#39;event-log-content&#39;);\n  const entry = document.createElement(&#39;div&#39;);\n  entry.className = &#39;log-entry&#39;;\n\n  const time = document.createElement(&#39;div&#39;);\n  time.className = &#39;log-time&#39;;\n  time.textContent = new Date().toLocaleTimeString();\n\n  const message = document.createElement(&#39;div&#39;);\n  message.className = &#39;log-message&#39;;\n  message.textContent = `${source}: ${action}${data ? ` - ${JSON.stringify(data)}` : &#39;&#39;}`;\n\n  entry.appendChild(time);\n  entry.appendChild(message);\n  logContainer.appendChild(entry);\n  logContainer.scrollTop = logContainer.scrollHeight;\n}\n\n// Custom Event Bus for cross-team communication\nclass EventBus extends EventTarget {\n  constructor() {\n    super();\n  }\n\n  publish(event, data) {\n    this.dispatchEvent(new CustomEvent(event, { detail: data }));\n    logEvent(&#39;EventBus&#39;, `Published &quot;${event}&quot;`, data);\n  }\n\n  subscribe(event, callback) {\n    this.addEventListener(event, e =&gt; callback(e.detail));\n    logEvent(&#39;EventBus&#39;, `Subscribed to &quot;${event}&quot;`);\n  }}\n\n\n// Create a shared event bus\nconst eventBus = new EventBus();\n\n// Mock User Data (in a real app, this would come from a backend)\nconst userData = {\n  accounts: [\n  { id: &#39;a001&#39;, type: &#39;Checking&#39;, name: &#39;Primary Checking&#39;, balance: 3240.56 },\n  { id: &#39;a002&#39;, type: &#39;Savings&#39;, name: &#39;Emergency Fund&#39;, balance: 15750.25 }],\n\n  investments: [\n  { id: &#39;i001&#39;, type: &#39;Stock&#39;, name: &#39;Tech Growth Fund&#39;, value: 8640.75, change: 12.3 },\n  { id: &#39;i002&#39;, type: &#39;Bond&#39;, name: &#39;Municipal Bond Fund&#39;, value: 12500.00, change: 3.2 }],\n\n  loans: [\n  { id: &#39;l001&#39;, type: &#39;Mortgage&#39;, name: &#39;Home Loan&#39;, balance: 245000.00, rate: 3.5 },\n  { id: &#39;l002&#39;, type: &#39;Auto&#39;, name: &#39;Car Loan&#39;, balance: 12500.00, rate: 4.2 }] };\n\n\n\n// Create the Accounts Dashboard Component\nclass AccountsDashboard extends HTMLElement {\n  constructor() {\n    super();\n    this.accounts = userData.accounts;\n  }\n\n  connectedCallback() {\n    logEvent(&#39;Accounts MFE&#39;, &#39;Initialized&#39;);\n    this.render();\n\n    // Listen for user selection events from other MFEs\n    eventBus.subscribe(&#39;user-selected&#39;, data =&gt; {\n      logEvent(&#39;Accounts MFE&#39;, &#39;Received user-selected event&#39;, data);\n      // In a real app, we would update the displayed accounts\n    });\n  }\n\n  render() {\n    this.innerHTML = `\n      &lt;div class=&quot;accounts-dashboard&quot;&gt;\n        &lt;h2&gt;Your Accounts&lt;/h2&gt;\n        &lt;div class=&quot;accounts-list&quot;&gt;\n          ${this.accounts.map(account =&gt; `\n            &lt;div class=&quot;account-card&quot;&gt;\n              &lt;div class=&quot;account-type&quot;&gt;${account.type}&lt;/div&gt;\n              &lt;div class=&quot;account-name&quot;&gt;${account.name}&lt;/div&gt;\n              &lt;div class=&quot;account-balance&quot;&gt;$${account.balance.toFixed(2)}&lt;/div&gt;\n              &lt;button class=&quot;view-details-btn&quot; data-id=&quot;${account.id}&quot;&gt;View Details&lt;/button&gt;\n            &lt;/div&gt;\n          `).join(&#39;&#39;)}\n        &lt;/div&gt;\n        &lt;div class=&quot;dashboard-actions&quot;&gt;\n          &lt;button class=&quot;primary-action&quot;&gt;Transfer Money&lt;/button&gt;\n          &lt;button&gt;Open New Account&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;style&gt;\n        .accounts-dashboard h2 {\n          color: var(--accounts-team-color);\n          margin-bottom: 20px;\n        }\n        \n        .accounts-list {\n          display: grid;\n          grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n          gap: 16px;\n          margin-bottom: 20px;\n        }\n        \n        .account-card {\n          background-color: white;\n          border-radius: 8px;\n          padding: 16px;\n          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n        }\n        \n        .account-type {\n          font-size: 12px;\n          color: var(--text-secondary);\n          margin-bottom: 4px;\n        }\n        \n        .account-name {\n          font-weight: 600;\n          margin-bottom: 12px;\n        }\n        \n        .account-balance {\n          font-size: 20px;\n          font-weight: 600;\n          color: var(--accounts-team-color);\n          margin-bottom: 16px;\n        }\n        \n        .view-details-btn {\n          display: block;\n          width: 100%;\n          padding: 8px;\n          background-color: var(--accounts-team-light);\n          color: var(--accounts-team-color);\n          border: 1px solid var(--accounts-team-color);\n          border-radius: 4px;\n          cursor: pointer;\n          font-family: inherit;\n          font-size: 14px;\n          transition: all 0.2s;\n        }\n        \n        .view-details-btn:hover {\n          background-color: var(--accounts-team-color);\n          color: white;\n        }\n        \n        .dashboard-actions {\n          display: flex;\n          gap: 12px;\n        }\n        \n        .dashboard-actions button {\n          padding: 8px 16px;\n          background-color: white;\n          border: 1px solid var(--border-color);\n          border-radius: 4px;\n          cursor: pointer;\n          font-family: inherit;\n        }\n        \n        .primary-action {\n          background-color: var(--accounts-team-color) !important;\n          color: white;\n          border-color: var(--accounts-team-color) !important;\n        }\n      &lt;/style&gt;\n    `;\n\n    // Add event listeners\n    this.querySelectorAll(&#39;.view-details-btn&#39;).forEach(btn =&gt; {\n      btn.addEventListener(&#39;click&#39;, event =&gt; {\n        const accountId = event.target.dataset.id;\n        const account = this.accounts.find(a =&gt; a.id === accountId);\n\n        logEvent(&#39;Accounts MFE&#39;, &#39;Account selected&#39;, { id: accountId });\n        eventBus.publish(&#39;account-selected&#39;, account);\n      });\n    });\n  }}\n\n\n// Create the Investments Summary Component\nclass InvestmentsSummary extends HTMLElement {\n  constructor() {\n    super();\n    this.investments = userData.investments;\n  }\n\n  connectedCallback() {\n    logEvent(&#39;Investments MFE&#39;, &#39;Initialized&#39;);\n    this.render();\n\n    // Listen for account selection events\n    eventBus.subscribe(&#39;account-selected&#39;, account =&gt; {\n      logEvent(&#39;Investments MFE&#39;, &#39;Received account-selected event&#39;, account);\n      // In a real app, we might show investment recommendations based on the account\n    });\n  }\n\n  render() {\n    this.innerHTML = `\n      &lt;div class=&quot;investments-summary&quot;&gt;\n        &lt;h2&gt;Your Investments&lt;/h2&gt;\n        &lt;div class=&quot;portfolio-summary&quot;&gt;\n          &lt;div class=&quot;portfolio-value&quot;&gt;\n            &lt;div class=&quot;summary-label&quot;&gt;Total Portfolio Value&lt;/div&gt;\n            &lt;div class=&quot;summary-value&quot;&gt;$${this.getTotalValue().toFixed(2)}&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;portfolio-performance&quot;&gt;\n            &lt;div class=&quot;summary-label&quot;&gt;Overall Performance&lt;/div&gt;\n            &lt;div class=&quot;summary-value ${this.getAverageChange() &gt;= 0 ? &#39;positive&#39; : &#39;negative&#39;}&quot;&gt;\n              ${this.getAverageChange() &gt;= 0 ? &#39;+&#39; : &#39;&#39;}${this.getAverageChange().toFixed(1)}%\n            &lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;investments-list&quot;&gt;\n          ${this.investments.map(inv =&gt; `\n            &lt;div class=&quot;investment-card&quot;&gt;\n              &lt;div class=&quot;investment-type&quot;&gt;${inv.type}&lt;/div&gt;\n              &lt;div class=&quot;investment-name&quot;&gt;${inv.name}&lt;/div&gt;\n              &lt;div class=&quot;investment-value&quot;&gt;$${inv.value.toFixed(2)}&lt;/div&gt;\n              &lt;div class=&quot;investment-change ${inv.change &gt;= 0 ? &#39;positive&#39; : &#39;negative&#39;}&quot;&gt;\n                ${inv.change &gt;= 0 ? &#39;+&#39; : &#39;&#39;}${inv.change}%\n              &lt;/div&gt;\n              &lt;button class=&quot;view-details-btn&quot; data-id=&quot;${inv.id}&quot;&gt;View Details&lt;/button&gt;\n            &lt;/div&gt;\n          `).join(&#39;&#39;)}\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;style&gt;\n        .investments-summary h2 {\n          color: var(--investments-team-color);\n          margin-bottom: 20px;\n        }\n        \n        .portfolio-summary {\n          display: flex;\n          gap: 20px;\n          margin-bottom: 24px;\n        }\n        \n        .portfolio-value, .portfolio-performance {\n          flex: 1;\n          background-color: white;\n          padding: 16px;\n          border-radius: 8px;\n          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n        }\n        \n        .summary-label {\n          font-size: 14px;\n          color: var(--text-secondary);\n          margin-bottom: 8px;\n        }\n        \n        .summary-value {\n          font-size: 24px;\n          font-weight: 600;\n        }\n        \n        .positive {\n          color: var(--accounts-team-color);\n        }\n        \n        .negative {\n          color: var(--support-team-color);\n        }\n        \n        .investments-list {\n          display: grid;\n          grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n          gap: 16px;\n        }\n        \n        .investment-card {\n          background-color: white;\n          border-radius: 8px;\n          padding: 16px;\n          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n        }\n        \n        .investment-type {\n          font-size: 12px;\n          color: var(--text-secondary);\n          margin-bottom: 4px;\n        }\n        \n        .investment-name {\n          font-weight: 600;\n          margin-bottom: 12px;\n        }\n        \n        .investment-value {\n          font-size: 18px;\n          font-weight: 600;\n          margin-bottom: 4px;\n        }\n        \n        .investment-change {\n          margin-bottom: 16px;\n          font-weight: 500;\n        }\n        \n        .view-details-btn {\n          display: block;\n          width: 100%;\n          padding: 8px;\n          background-color: var(--investments-team-light);\n          color: var(--investments-team-color);\n          border: 1px solid var(--investments-team-color);\n          border-radius: 4px;\n          cursor: pointer;\n          font-family: inherit;\n          font-size: 14px;\n          transition: all 0.2s;\n        }\n        \n        .view-details-btn:hover {\n          background-color: var(--investments-team-color);\n          color: white;\n        }\n      &lt;/style&gt;\n    `;\n\n    // Add event listeners\n    this.querySelectorAll(&#39;.view-details-btn&#39;).forEach(btn =&gt; {\n      btn.addEventListener(&#39;click&#39;, event =&gt; {\n        const investmentId = event.target.dataset.id;\n        const investment = this.investments.find(i =&gt; i.id === investmentId);\n\n        logEvent(&#39;Investments MFE&#39;, &#39;Investment selected&#39;, { id: investmentId });\n        eventBus.publish(&#39;investment-selected&#39;, investment);\n      });\n    });\n  }\n\n  getTotalValue() {\n    return this.investments.reduce((sum, inv) =&gt; sum + inv.value, 0);\n  }\n\n  getAverageChange() {\n    return this.investments.reduce((sum, inv) =&gt; sum + inv.change, 0) / this.investments.length;\n  }}\n\n\n// Create the Loans Overview Component\nclass LoansOverview extends HTMLElement {\n  constructor() {\n    super();\n    this.loans = userData.loans;\n  }\n\n  connectedCallback() {\n    logEvent(&#39;Loans MFE&#39;, &#39;Initialized&#39;);\n    this.render();\n\n    // Listen for investment selection events\n    eventBus.subscribe(&#39;investment-selected&#39;, investment =&gt; {\n      logEvent(&#39;Loans MFE&#39;, &#39;Received investment-selected event&#39;, investment);\n      // In a real app, we might show loan refinancing options based on investment performance\n    });\n  }\n\n  render() {\n    this.innerHTML = `\n      &lt;div class=&quot;loans-overview&quot;&gt;\n        &lt;h2&gt;Your Loans&lt;/h2&gt;\n        &lt;div class=&quot;loans-summary&quot;&gt;\n          &lt;div class=&quot;total-owed&quot;&gt;\n            &lt;div class=&quot;summary-label&quot;&gt;Total Balance&lt;/div&gt;\n            &lt;div class=&quot;summary-value&quot;&gt;$${this.getTotalBalance().toFixed(2)}&lt;/div&gt;\n          &lt;/div&gt;\n          &lt;div class=&quot;avg-rate&quot;&gt;\n            &lt;div class=&quot;summary-label&quot;&gt;Average Rate&lt;/div&gt;\n            &lt;div class=&quot;summary-value&quot;&gt;${this.getAverageRate().toFixed(1)}%&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;loans-list&quot;&gt;\n          ${this.loans.map(loan =&gt; `\n            &lt;div class=&quot;loan-card&quot;&gt;\n              &lt;div class=&quot;loan-type&quot;&gt;${loan.type}&lt;/div&gt;\n              &lt;div class=&quot;loan-name&quot;&gt;${loan.name}&lt;/div&gt;\n              &lt;div class=&quot;loan-details&quot;&gt;\n                &lt;div&gt;\n                  &lt;span class=&quot;detail-label&quot;&gt;Balance:&lt;/span&gt;\n                  &lt;span class=&quot;detail-value&quot;&gt;$${loan.balance.toFixed(2)}&lt;/span&gt;\n                &lt;/div&gt;\n                &lt;div&gt;\n                  &lt;span class=&quot;detail-label&quot;&gt;Rate:&lt;/span&gt;\n                  &lt;span class=&quot;detail-value&quot;&gt;${loan.rate}%&lt;/span&gt;\n                &lt;/div&gt;\n              &lt;/div&gt;\n              &lt;button class=&quot;view-details-btn&quot; data-id=&quot;${loan.id}&quot;&gt;View Details&lt;/button&gt;\n            &lt;/div&gt;\n          `).join(&#39;&#39;)}\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;loans-actions&quot;&gt;\n          &lt;button class=&quot;primary-action&quot;&gt;Make a Payment&lt;/button&gt;\n          &lt;button&gt;Refinance Options&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;style&gt;\n        .loans-overview h2 {\n          color: var(--loans-team-color);\n          margin-bottom: 20px;\n        }\n        \n        .loans-summary {\n          display: flex;\n          gap: 20px;\n          margin-bottom: 24px;\n        }\n        \n        .total-owed, .avg-rate {\n          flex: 1;\n          background-color: white;\n          padding: 16px;\n          border-radius: 8px;\n          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n        }\n        \n        .summary-label {\n          font-size: 14px;\n          color: var(--text-secondary);\n          margin-bottom: 8px;\n        }\n        \n        .summary-value {\n          font-size: 24px;\n          font-weight: 600;\n        }\n        \n        .loans-list {\n          display: grid;\n          grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n          gap: 16px;\n          margin-bottom: 20px;\n        }\n        \n        .loan-card {\n          background-color: white;\n          border-radius: 8px;\n          padding: 16px;\n          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n        }\n        \n        .loan-type {\n          font-size: 12px;\n          color: var(--text-secondary);\n          margin-bottom: 4px;\n        }\n        \n        .loan-name {\n          font-weight: 600;\n          margin-bottom: 12px;\n        }\n        \n        .loan-details {\n          margin-bottom: 16px;\n        }\n        \n        .detail-label {\n          color: var(--text-secondary);\n          margin-right: 4px;\n        }\n        \n        .detail-value {\n          font-weight: 500;\n        }\n        \n        .view-details-btn {\n          display: block;\n          width: 100%;\n          padding: 8px;\n          background-color: var(--loans-team-light);\n          color: var(--loans-team-color);\n          border: 1px solid var(--loans-team-color);\n          border-radius: 4px;\n          cursor: pointer;\n          font-family: inherit;\n          font-size: 14px;\n          transition: all 0.2s;\n        }\n        \n        .view-details-btn:hover {\n          background-color: var(--loans-team-color);\n          color: white;\n        }\n        \n        .loans-actions {\n          display: flex;\n          gap: 12px;\n        }\n        \n        .loans-actions button {\n          padding: 8px 16px;\n          background-color: white;\n          border: 1px solid var(--border-color);\n          border-radius: 4px;\n          cursor: pointer;\n          font-family: inherit;\n        }\n        \n        .primary-action {\n          background-color: var(--loans-team-color) !important;\n          color: white;\n          border-color: var(--loans-team-color) !important;\n        }\n      &lt;/style&gt;\n    `;\n\n    // Add event listeners\n    this.querySelectorAll(&#39;.view-details-btn&#39;).forEach(btn =&gt; {\n      btn.addEventListener(&#39;click&#39;, event =&gt; {\n        const loanId = event.target.dataset.id;\n        const loan = this.loans.find(l =&gt; l.id === loanId);\n\n        logEvent(&#39;Loans MFE&#39;, &#39;Loan selected&#39;, { id: loanId });\n        eventBus.publish(&#39;loan-selected&#39;, loan);\n      });\n    });\n  }\n\n  getTotalBalance() {\n    return this.loans.reduce((sum, loan) =&gt; sum + loan.balance, 0);\n  }\n\n  getAverageRate() {\n    return this.loans.reduce((sum, loan) =&gt; sum + loan.rate, 0) / this.loans.length;\n  }}\n\n\n// Create the Support Widget Component\nclass SupportWidget extends HTMLElement {\n  constructor() {\n    super();\n  }\n\n  connectedCallback() {\n    logEvent(&#39;Support MFE&#39;, &#39;Initialized&#39;);\n    this.render();\n\n    // Listen for various events to provide context-aware support\n    eventBus.subscribe(&#39;account-selected&#39;, account =&gt; {\n      this.updateSupportContext(&#39;account&#39;, account);\n    });\n\n    eventBus.subscribe(&#39;investment-selected&#39;, investment =&gt; {\n      this.updateSupportContext(&#39;investment&#39;, investment);\n    });\n\n    eventBus.subscribe(&#39;loan-selected&#39;, loan =&gt; {\n      this.updateSupportContext(&#39;loan&#39;, loan);\n    });\n  }\n\n  updateSupportContext(type, data) {\n    logEvent(&#39;Support MFE&#39;, `Context updated to ${type}`, data);\n\n    const contextDisplay = this.querySelector(&#39;.context-aware-support&#39;);\n    if (contextDisplay) {\n      let contextText = &#39;&#39;;\n\n      switch (type) {\n        case &#39;account&#39;:\n          contextText = `Need help with your ${data.name} account?`;\n          break;\n        case &#39;investment&#39;:\n          contextText = `Questions about your ${data.name} investment?`;\n          break;\n        case &#39;loan&#39;:\n          contextText = `Need assistance with your ${data.name}?`;\n          break;}\n\n\n      contextDisplay.textContent = contextText;\n      contextDisplay.style.display = &#39;block&#39;;\n    }\n  }\n\n  render() {\n    this.innerHTML = `\n      &lt;div class=&quot;support-widget&quot;&gt;\n        &lt;h2&gt;Customer Support&lt;/h2&gt;\n        \n        &lt;div class=&quot;context-aware-support&quot; style=&quot;display: none;&quot;&gt;\n          &lt;!-- Dynamically updated based on user interactions --&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;support-options&quot;&gt;\n          &lt;div class=&quot;support-option&quot;&gt;\n            &lt;div class=&quot;option-icon&quot;&gt;📞&lt;/div&gt;\n            &lt;div class=&quot;option-details&quot;&gt;\n              &lt;div class=&quot;option-title&quot;&gt;Call Us&lt;/div&gt;\n              &lt;div class=&quot;option-description&quot;&gt;Speak with a representative&lt;/div&gt;\n              &lt;div class=&quot;option-action&quot;&gt;1-800-555-0123&lt;/div&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;support-option&quot;&gt;\n            &lt;div class=&quot;option-icon&quot;&gt;💬&lt;/div&gt;\n            &lt;div class=&quot;option-details&quot;&gt;\n              &lt;div class=&quot;option-title&quot;&gt;Live Chat&lt;/div&gt;\n              &lt;div class=&quot;option-description&quot;&gt;Chat with a support agent&lt;/div&gt;\n              &lt;button class=&quot;option-button&quot;&gt;Start Chat&lt;/button&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;support-option&quot;&gt;\n            &lt;div class=&quot;option-icon&quot;&gt;✉️&lt;/div&gt;\n            &lt;div class=&quot;option-details&quot;&gt;\n              &lt;div class=&quot;option-title&quot;&gt;Email&lt;/div&gt;\n              &lt;div class=&quot;option-description&quot;&gt;Send us a message&lt;/div&gt;\n              &lt;button class=&quot;option-button&quot;&gt;Contact Form&lt;/button&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;faq-section&quot;&gt;\n          &lt;h3&gt;Frequently Asked Questions&lt;/h3&gt;\n          &lt;ul class=&quot;faq-list&quot;&gt;\n            &lt;li&gt;&lt;a href=&quot;#&quot; class=&quot;faq-link&quot;&gt;How do I reset my password?&lt;/a&gt;&lt;/li&gt;\n            &lt;li&gt;&lt;a href=&quot;#&quot; class=&quot;faq-link&quot;&gt;How to dispute a transaction?&lt;/a&gt;&lt;/li&gt;\n            &lt;li&gt;&lt;a href=&quot;#&quot; class=&quot;faq-link&quot;&gt;What are the wire transfer fees?&lt;/a&gt;&lt;/li&gt;\n            &lt;li&gt;&lt;a href=&quot;#&quot; class=&quot;faq-link&quot;&gt;How to order a replacement card?&lt;/a&gt;&lt;/li&gt;\n          &lt;/ul&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;style&gt;\n        .support-widget h2 {\n          color: var(--support-team-color);\n          margin-bottom: 20px;\n        }\n        \n        .context-aware-support {\n          background-color: var(--support-team-light);\n          border-left: 4px solid var(--support-team-color);\n          padding: 12px 16px;\n          margin-bottom: 20px;\n          border-radius: 0 4px 4px 0;\n          font-weight: 500;\n        }\n        \n        .support-options {\n          display: grid;\n          grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n          gap: 16px;\n          margin-bottom: 24px;\n        }\n        \n        .support-option {\n          background-color: white;\n          border-radius: 8px;\n          padding: 16px;\n          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n          display: flex;\n          gap: 12px;\n        }\n        \n        .option-icon {\n          font-size: 24px;\n        }\n        \n        .option-details {\n          flex: 1;\n        }\n        \n        .option-title {\n          font-weight: 600;\n          margin-bottom: 4px;\n        }\n        \n        .option-description {\n          color: var(--text-secondary);\n          font-size: 14px;\n          margin-bottom: 12px;\n        }\n        \n        .option-action {\n          font-weight: 500;\n          color: var(--support-team-color);\n        }\n        \n        .option-button {\n          padding: 6px 12px;\n          background-color: var(--support-team-color);\n          color: white;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n          font-family: inherit;\n          font-size: 14px;\n        }\n        \n        .faq-section h3 {\n          margin-bottom: 12px;\n          font-size: 16px;\n        }\n        \n        .faq-list {\n          list-style: none;\n          padding: 0;\n        }\n        \n        .faq-list li {\n          margin-bottom: 8px;\n        }\n        \n        .faq-link {\n          color: var(--support-team-color);\n          text-decoration: none;\n        }\n        \n        .faq-link:hover {\n          text-decoration: underline;\n        }\n      &lt;/style&gt;\n    `;\n\n    // Add event listeners\n    this.querySelectorAll(&#39;.option-button&#39;).forEach(btn =&gt; {\n      btn.addEventListener(&#39;click&#39;, event =&gt; {\n        const action = event.target.textContent;\n        logEvent(&#39;Support MFE&#39;, `Support action initiated: ${action}`);\n      });\n    });\n\n    this.querySelectorAll(&#39;.faq-link&#39;).forEach(link =&gt; {\n      link.addEventListener(&#39;click&#39;, event =&gt; {\n        event.preventDefault();\n        const question = event.target.textContent;\n        logEvent(&#39;Support MFE&#39;, `FAQ clicked: ${question}`);\n      });\n    });\n  }}\n\n\n// Register the custom elements\ncustomElements.define(&#39;accounts-dashboard&#39;, AccountsDashboard);\ncustomElements.define(&#39;investments-summary&#39;, InvestmentsSummary);\ncustomElements.define(&#39;loans-overview&#39;, LoansOverview);\ncustomElements.define(&#39;support-widget&#39;, SupportWidget);\n\n// Set up navigation\ndocument.addEventListener(&#39;DOMContentLoaded&#39;, () =&gt; {\n  const navItems = document.querySelectorAll(&#39;.nav-item&#39;);\n\n  navItems.forEach(item =&gt; {\n    item.addEventListener(&#39;click&#39;, () =&gt; {\n      // Update active nav item\n      navItems.forEach(nav =&gt; nav.classList.remove(&#39;active&#39;));\n      item.classList.add(&#39;active&#39;);\n\n      // Show corresponding section\n      const section = item.dataset.section;\n      document.querySelectorAll(&#39;.content-area section&#39;).forEach(s =&gt; {\n        s.classList.remove(&#39;active&#39;);\n      });\n      document.getElementById(`microfrontend-${section}`).classList.add(&#39;active&#39;);\n\n      logEvent(&#39;Shell Application&#39;, `Navigated to ${section} section`);\n    });\n  });\n\n  // Log initialization\n  logEvent(&#39;Shell Application&#39;, &#39;Micro-Frontend demo initialized&#39;);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\">the original article</a>.</p></figure>\n<p>This simplified banking portal demonstrates the core principles of micro-frontend architecture:</p>\n<ol>\n<li>\n<p><strong>Team Ownership Separation</strong> - Each colored section is owned by a different team</p>\n</li>\n<li>\n<p><strong>Independent Development</strong> - Each team can build and deploy their component independently</p>\n</li>\n<li>\n<p><strong>Cross-Team Communication</strong> - Components interact through a centralized event bus</p>\n</li>\n<li>\n<p><strong>Consistent User Experience</strong> - Despite being developed separately, components provide a unified experience</p>\n</li>\n</ol>\n<p>Try interacting with different components and watch the event log to see how they communicate. When you click on an account in the Accounts section, notice how the Support widget responds with contextual information - demonstrating cross-component communication.</p>\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>You can now do four concrete things with a web component:</p>\n<ul>\n<li><strong>Compose</strong> it from slots so the page decides the content and the component decides the layout.</li>\n<li><strong>Pick the right input:</strong> attributes for small string settings, properties for arrays and objects.</li>\n<li><strong>Send events out</strong> with <code>bubbles: true</code> and <code>composed: true</code> so listeners outside the shadow root hear them.</li>\n<li><strong>Keep names predictable</strong> (<code>variant</code>, <code>size</code>, <code>disabled</code>) so one component teaches you the next.</li>\n</ul>\n<p>The sections on lazy-loading, constructable stylesheets, accessibility, framework wrappers and micro-frontends are worked examples of those four habits under pressure; <a href=\"https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/\">the post on frameworks</a> takes the framework wrappers further. The last post in the series is a dated status board of what ships and what does not.</p>\n<h2 id=\"resources\">Resources<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#resources\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/Web_Components\">Web Components on MDN</a></p>\n</li>\n<li>\n<p><a href=\"https://lit.dev/\">Google's Lit library</a></p>\n</li>\n<li>\n<p><a href=\"https://www.fast.design/\">Microsoft's FAST components</a></p>\n</li>\n<li>\n<p><a href=\"https://opensource.adobe.com/spectrum-web-components/\">Adobe's Spectrum Web Components</a></p>\n</li>\n<li>\n<p><a href=\"https://shoelace.style/\">Shoelace components</a></p>\n</li>\n<li>\n<p><a href=\"https://www.webcomponents.org/\">WebComponents.org</a></p>\n</li>\n</ul>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Real-World Examples & Patterns in Web Components\nCanonical: https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/\nCustom Elements for Scalable and Maintainable Web Applications\nIn today's diverse front-end landscape, Web Components stand out as a platform-native solution for creating reusable UI elements. While frameworks come and go, Web Components offer a standardized approach that works across codebases and technologies. But where can we see them in action? Who's using them in production?\nThis post shows who publishes components such as <user-card>, and the four habits (composition, property against attribute, events, consistent names) that make them pleasant to use.\nWho ships them: the clearest public evidence is open-source design systems and libraries you can read yourself. Google maintains Lit (https://lit.dev/), Adobe publishes Spectrum Web Components (https://opensource.adobe.com/spectrum-web-components/), Microsoft publishes FAST (https://www.fast.design/) and Fluent UI web components, IBM's Carbon design system has a web components package, and Salesforce's Lightning Web Components are built on the same standards. This post names only what is publicly documented and makes no claims about closed-source internals.\nRecap if you are joining here: a custom element is a JavaScript class registered with customElements.define(), usually with a shadow root that keeps its styles private. Let's start with a basic example. This demo shows the lifecycle methods: you can see how the component reacts when it's added to the DOM, removed, or when its attributes change:\nHTML\n<div class=\"container\">\n<h2>Basic Web Component Demo</h2>\n<div class=\"controls\">\n<label for=\"name-input\">Change the name:</label>\n<input id=\"name-input\" type=\"text\" value=\"World\">\n<button id=\"update-btn\">Update</button>\n<button id=\"add-btn\">Add to DOM</button>\n<button id=\"remove-btn\">Remove from DOM</button>\n</div>\n<div class=\"component-container\" id=\"component-container\">\n<!-- The component will be added/removed here -->\n</div>\n<div class=\"lifecycle-log\">\n<h3>Lifecycle Events:</h3>\n<div id=\"lifecycle-events\"></div>\n<button id=\"clear-log\">Clear Log</button>\n</div>\n</div>\nCSS\n.container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 600px;\nmargin: 0 auto;\npadding: 20px;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: #f5f5f5;\nborder-radius: 5px;\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\nalign-items: center;\n}\n.component-container {\nborder: 2px dashed #ccc;\npadding: 20px;\nmin-height: 100px;\nmargin-bottom: 20px;\nborder-radius: 5px;\n}\n.lifecycle-log {\nbackground-color: #f8f8f8;\npadding: 15px;\nborder-radius: 5px;\n}\n#lifecycle-events {\nmax-height: 200px;\noverflow-y: auto;\nmargin-bottom: 10px;\nfont-family: monospace;\nfont-size: 14px;\npadding: 10px;\nbackground-color: #eee;\nborder-radius: 3px;\n}\n.log-entry {\nmargin-bottom: 5px;\npadding: 3px;\nborder-bottom: 1px solid #ddd;\n}\n.log-entry.constructor { color: #0066cc; }\n.log-entry.connected { color: #28a745; }\n.log-entry.disconnected { color: #dc3545; }\n.log-entry.attribute-changed { color: #fd7e14; }\nbutton {\npadding: 8px 12px;\nbackground-color: #4a6cf7;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 14px;\n}\nbutton:hover {\nbackground-color: #3a5cf7;\n}\n#clear-log {\nbackground-color: #6c757d;\n}\ninput {\npadding: 8px;\nborder: 1px solid #ccc;\nborder-radius: 4px;\nfont-size: 14px;\n}\nJavaScript\n// Define the custom element\nclass HelloWorld extends HTMLElement {\n// Specify observed attributes for attributeChangedCallback\nstatic get observedAttributes() {\nreturn ['name'];\n}\nconstructor() {\n// Always call super() first in constructor\nsuper();\n// Create a shadow root\nconst shadow = this.attachShadow({ mode: 'open' });\n// Create element contents\nconst wrapper = document.createElement('div');\nwrapper.setAttribute('class', 'hello-wrapper');\nthis.greeting = document.createElement('h3');\nthis.greeting.setAttribute('class', 'greeting');\n// Apply styles\nconst style = document.createElement('style');\nstyle.textContent = `\n.hello-wrapper {\npadding: 20px;\nbackground: linear-gradient(135deg, #6e8efb, #a777e3);\nborder-radius: 8px;\nbox-shadow: 0 4px 8px rgba(0,0,0,0.1);\ncolor: white;\ntext-align: center;\ntransition: transform 0.3s ease;\n}\n.hello-wrapper:hover {\ntransform: translateY(-5px);\n}\n.greeting {\nmargin: 0;\nfont-weight: 500;\n}\n`;\n// Attach elements to shadow DOM\nshadow.appendChild(style);\nshadow.appendChild(wrapper);\nwrapper.appendChild(this.greeting);\n// Log constructor call\nthis.logLifecycleEvent('constructor', 'Component created');\n}\n// Called when element is inserted into the DOM\nconnectedCallback() {\n// Update the greeting text\nthis.updateGreeting();\nthis.logLifecycleEvent('connected', 'Component added to DOM');\n}\n// Called when element is removed from the DOM\ndisconnectedCallback() {\nthis.logLifecycleEvent('disconnected', 'Component removed from DOM');\n}\n// Called when one of the observed attributes changes\nattributeChangedCallback(name, oldValue, newValue) {\nif (oldValue !== newValue) {\nthis.logLifecycleEvent(\n'attribute-changed',\n`Attribute \"${name}\" changed from \"${oldValue || 'none'}\" to \"${newValue}\"`);\nif (name === 'name') {\nthis.updateGreeting();\n}\n}\n}\n// Helper method to update the greeting text\nupdateGreeting() {\nconst name = this.getAttribute('name') || 'World';\nthis.greeting.textContent = `Hello, ${name}!`;\n}\n// Helper method to log lifecycle events\nlogLifecycleEvent(type, message) {\nconst logContainer = document.getElementById('lifecycle-events');\nif (logContainer) {\nconst entry = document.createElement('div');\nentry.className = `log-entry ${type}`;\nentry.textContent = `${new Date().toLocaleTimeString()} - ${message}`;\nlogContainer.appendChild(entry);\nlogContainer.scrollTop = logContainer.scrollHeight;\n}\n}}\n// Register the custom element\ncustomElements.define('hello-world', HelloWorld);\n// Setup UI interaction\ndocument.addEventListener('DOMContentLoaded', () => {\nconst container = document.getElementById('component-container');\nconst nameInput = document.getElementById('name-input');\nconst updateBtn = document.getElementById('update-btn');\nconst addBtn = document.getElementById('add-btn');\nconst removeBtn = document.getElementById('remove-btn');\nconst clearLogBtn = document.getElementById('clear-log');\nconst logContainer = document.getElementById('lifecycle-events');\nlet component = null;\n// Add component to DOM\naddBtn.addEventListener('click', () => {\nif (!container.contains(component)) {\ncomponent = document.createElement('hello-world');\ncomponent.setAttribute('name', nameInput.value);\ncontainer.appendChild(component);\n}\n});\n// Remove component from DOM\nremoveBtn.addEventListener('click', () => {\nif (component && container.contains(component)) {\ncontainer.removeChild(component);\ncomponent = null;\n}\n});\n// Update name attribute\nupdateBtn.addEventListener('click', () => {\nif (component) {\ncomponent.setAttribute('name', nameInput.value);\n}\n});\n// Clear log\nclearLogBtn.addEventListener('click', () => {\nlogContainer.innerHTML = '';\n});\n// Initial log message\nconst entry = document.createElement('div');\nentry.className = 'log-entry';\nentry.textContent = `${new Date().toLocaleTimeString()} - Demo initialized. Click \"Add to DOM\" to create the component.`;\nlogContainer.appendChild(entry);\n});\nBasic Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nTry clicking the buttons to see the lifecycle methods in action. This simple example demonstrates the core concepts of Web Components: custom elements, shadow DOM encapsulation, and lifecycle callbacks.\nWho's Using Web Components Today?\nThe short answer is at the top of this post. Here is what each name actually offers, limited to things you can install and read:\nGoogle: maintains Lit, and Material Web (@material/web), Google's Material Design components as web components.\nMicrosoft: publishes the FAST library and Fluent UI web components.\nAdobe: Spectrum Web Components, the web implementation of its Spectrum design system.\nIBM: the Carbon design system has a web components package.\nSalesforce: Lightning Web Components is a framework built on the web components standards.\nGitHub: has published small custom elements as open source (for example <relative-time>).\nING: publishes the open-source Lion web components library.\nClaims that a particular big site (a video player, a map, an editor, a streaming service) is \"built with web components\" are common online. They are not backed by public sources, so this post leaves them out. Check a site's own engineering posts before repeating one.\nPopular Web Component Libraries & Hosting Strategies\nLibraries and Frameworks\nSeveral libraries have emerged to simplify Web Component development:\nLit: Google's lightweight library (successor to Polymer) for creating Web Components with minimal boilerplate.\nStencil: Created by the Ionic team, Stencil generates highly optimized Web Components and can output framework-specific wrappers.\nFAST: Microsoft's adaptive UI library produces Web Components that automatically adapt to user preferences.\nShoelace: A collection of professionally designed, every day UI components built on a framework-agnostic technology.\nEnhance: A framework that uses progressive enhancement to create multi-page applications with Web Components.\nHosting & Distribution Strategies\nCompanies distribute their Web Components in several ways:\nNPM Packages: The most common approach, allowing developers to install components via package managers.\nExample: npm install @adobe/spectrum-web-components\nCDN Delivery: Pre-built components hosted on CDNs allow for quick inclusion without build steps.\nExample: <script type=\"module\" src=\"https://unpkg.com/@shoelace-style/shoelace@2.0.0/dist/shoelace.js\"></script>\nCustom Element Registries: Specialized registries like webcomponents.org (https://www.webcomponents.org/) catalog available components.\nBundled with Applications: Many applications bundle custom elements as part of their build process.\nMicro-frontend Architecture: Components distributed across multiple teams but united in a single application.\nCommon Patterns in Production Web Components\nComposition Pattern\nOne of the most powerful patterns is component composition:\n<!-- Container component -->\n<user-profile>\n<!-- Composable child components -->\n<user-avatar slot=\"avatar\"></user-avatar>\n<user-info slot=\"info\"></user-info>\n<user-stats slot=\"stats\"></user-stats>\n</user-profile>\nThis pattern enables teams to build complex UIs from simpler building blocks, a strategy employed by many design systems.\nTo see the composition pattern in action, here's an interactive demo that demonstrates how slots distribute content in Web Components:\nHTML\n<div class=\"container\">\n<h2>Web Component Composition Pattern Demo</h2>\n<div class=\"explanation\">\n<p>This demo implements the <code>&lt;user-profile&gt;</code> composition pattern shown in the example above.</p>\n<p>Toggle sections to see how content is distributed into different slots within the component.</p>\n</div>\n<div class=\"controls\">\n<button id=\"toggle-avatar\" class=\"active\">Toggle Avatar</button>\n<button id=\"toggle-info\" class=\"active\">Toggle Info</button>\n<button id=\"toggle-stats\" class=\"active\">Toggle Stats</button>\n<button id=\"toggle-theme\">Change Theme</button>\n</div>\n<div class=\"demo-container\">\n<div class=\"component-view\">\n<h3>Component Preview</h3>\n<div class=\"profile-container\">\n<!-- The actual user-profile component -->\n<user-profile theme=\"light\">\n<!-- Avatar slot content -->\n<div slot=\"avatar\" id=\"avatar-content\">\n<img src=\"https://randomuser.me/api/portraits/women/42.jpg\" alt=\"User avatar\">\n</div>\n<!-- Info slot content -->\n<div slot=\"info\" id=\"info-content\">\n<h3>Sarah Johnson</h3>\n<p class=\"job-title\">Senior Web Developer</p>\n<p class=\"location\">San Francisco, CA</p>\n</div>\n<!-- Stats slot content -->\n<div slot=\"stats\" id=\"stats-content\">\n<div class=\"stat-item\">\n<span class=\"stat-value\">142</span>\n<span class=\"stat-label\">Projects</span>\n</div>\n<div class=\"stat-item\">\n<span class=\"stat-value\">3.2K</span>\n<span class=\"stat-label\">Followers</span>\n</div>\n<div class=\"stat-item\">\n<span class=\"stat-value\">87</span>\n<span class=\"stat-label\">Reviews</span>\n</div>\n</div>\n</user-profile>\n</div>\n</div>\n<div class=\"structure-diagram\">\n<h3>Component Structure</h3>\n<div class=\"diagram\">\n<div class=\"shadow-dom\">\n<div class=\"shadow-label\">Shadow DOM</div>\n<div class=\"slot-container\">\n<div class=\"slot avatar-slot\">\n<span class=\"slot-label\">slot=\"avatar\"</span>\n<div class=\"slot-content\" id=\"avatar-indicator\">Content from Light DOM</div>\n</div>\n<div class=\"slot info-slot\">\n<span class=\"slot-label\">slot=\"info\"</span>\n<div class=\"slot-content\" id=\"info-indicator\">Content from Light DOM</div>\n</div>\n<div class=\"slot stats-slot\">\n<span class=\"slot-label\">slot=\"stats\"</span>\n<div class=\"slot-content\" id=\"stats-indicator\">Content from Light DOM</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"code-preview\">\n<h3>HTML Code</h3>\n<pre><code id=\"code-output\"></code></pre>\n</div>\n</div>\nCSS\n:root {\n--primary-color: #4a6cf7;\n--primary-light: #e0e7ff;\n--avatar-slot-color: #ec4899;\n--info-slot-color: #10b981;\n--stats-slot-color: #f59e0b;\n--text-dark: #1f2937;\n--text-light: #f9fafb;\n--bg-light: #ffffff;\n--bg-dark: #1f2937;\n--border-light: #e5e7eb;\n--border-dark: #4b5563;\n--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n* {\nbox-sizing: border-box;\nmargin: 0;\npadding: 0;\n}\nbody {\nfont-family: var(--font-family);\ncolor: var(--text-dark);\nbackground-color: #f3f4f6;\nline-height: 1.5;\npadding: 20px;\n}\n.container {\nmax-width: 900px;\nmargin: 0 auto;\n}\nh2 {\ncolor: var(--primary-color);\nmargin-bottom: 24px;\ntext-align: center;\n}\nh3 {\nmargin-bottom: 16px;\nfont-weight: 600;\n}\n.explanation {\nbackground-color: var(--primary-light);\nborder-left: 4px solid var(--primary-color);\npadding: 16px;\nmargin-bottom: 24px;\nborder-radius: 0 8px 8px 0;\n}\n.controls {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\nmargin-bottom: 24px;\n}\nbutton {\npadding: 8px 16px;\nbackground-color: #f3f4f6;\nborder: 1px solid #d1d5db;\nborder-radius: 6px;\nfont-family: inherit;\nfont-size: 14px;\ncursor: pointer;\ntransition: all 0.2s;\n}\nbutton:hover {\nbackground-color: #e5e7eb;\n}\nbutton.active {\nbackground-color: var(--primary-color);\ncolor: white;\nborder-color: var(--primary-color);\n}\n.demo-container {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 24px;\nmargin-bottom: 24px;\n}\n@media (max-width: 768px) {\n.demo-container {\ngrid-template-columns: 1fr;\n}\n}\n.component-view, .structure-diagram {\nbackground-color: white;\nborder-radius: 8px;\npadding: 20px;\nbox-shadow: var(--shadow);\n}\n.profile-container {\nborder: 2px dashed #d1d5db;\npadding: 20px;\nborder-radius: 8px;\ndisplay: flex;\njustify-content: center;\n}\n/* Component styling */\nuser-profile {\ndisplay: block;\nwidth: 100%;\nmax-width: 400px;\ntransition: all 0.3s ease;\n}\n/* Light DOM content styling */\n[slot=\"avatar\"] img {\nwidth: 100px;\nheight: 100px;\nborder-radius: 50%;\nobject-fit: cover;\n}\n[slot=\"info\"] h3 {\nmargin: 0 0 8px 0;\nfont-size: 20px;\n}\n.job-title {\ncolor: #4b5563;\nmargin-bottom: 4px;\n}\n.location {\ncolor: #6b7280;\nfont-size: 14px;\n}\n.stat-item {\ntext-align: center;\n}\n.stat-value {\ndisplay: block;\nfont-size: 20px;\nfont-weight: 600;\ncolor: var(--primary-color);\n}\n.stat-label {\nfont-size: 14px;\ncolor: #6b7280;\n}\n/* Structure diagram styling */\n.diagram {\nborder: 2px dashed #d1d5db;\npadding: 20px;\nborder-radius: 8px;\n}\n.shadow-dom {\nborder: 2px solid var(--primary-color);\nborder-radius: 8px;\npadding: 20px;\nposition: relative;\n}\n.shadow-label {\nposition: absolute;\ntop: -12px;\nleft: 16px;\nbackground-color: var(--primary-color);\ncolor: white;\npadding: 2px 8px;\nborder-radius: 4px;\nfont-size: 14px;\n}\n.slot-container {\ndisplay: flex;\nflex-direction: column;\ngap: 16px;\nmargin-top: 12px;\n}\n.slot {\nborder: 2px solid;\nborder-radius: 6px;\npadding: 12px;\nposition: relative;\ntransition: all 0.3s ease;\n}\n.slot.empty {\nopacity: 0.6;\nbackground-color: #f9fafb;\n}\n.avatar-slot {\nborder-color: var(--avatar-slot-color);\n}\n.info-slot {\nborder-color: var(--info-slot-color);\n}\n.stats-slot {\nborder-color: var(--stats-slot-color);\n}\n.slot-label {\nposition: absolute;\ntop: -10px;\nleft: 10px;\npadding: 0 6px;\nborder-radius: 4px;\nfont-size: 12px;\nfont-weight: 500;\ncolor: white;\n}\n.avatar-slot .slot-label {\nbackground-color: var(--avatar-slot-color);\n}\n.info-slot .slot-label {\nbackground-color: var(--info-slot-color);\n}\n.stats-slot .slot-label {\nbackground-color: var(--stats-slot-color);\n}\n.slot-content {\nfont-family: monospace;\nfont-size: 14px;\ncolor: #6b7280;\n}\n/* Code preview styling */\n.code-preview {\nbackground-color: white;\nborder-radius: 8px;\npadding: 20px;\nbox-shadow: var(--shadow);\n}\npre {\nbackground-color: #1e293b;\ncolor: #e2e8f0;\npadding: 16px;\nborder-radius: 6px;\noverflow-x: auto;\nfont-family: monospace;\nfont-size: 14px;\nline-height: 1.5;\n}\n/* Animation for slot content highlight */\n@keyframes highlight {\n0% { background-color: rgba(255, 255, 100, 0.3); }\n100% { background-color: transparent; }\n}\n.highlight {\nanimation: highlight 1s ease;\n}\nJavaScript\n// Define the UserProfile Component\nclass UserProfile extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Initialize properties\nthis._theme = this.getAttribute('theme') || 'light';\n// Render the component\nthis.render();\n}\n// Observed attributes\nstatic get observedAttributes() {\nreturn ['theme'];\n}\n// Handle attribute changes\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'theme' && oldValue !== newValue) {\nthis._theme = newValue;\nthis.render();\n}\n}\n// Render the component\nrender() {\nconst isDark = this._theme === 'dark';\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.profile {\nbackground-color: ${isDark ? 'var(--bg-dark, #1f2937)' : 'var(--bg-light, #ffffff)'};\ncolor: ${isDark ? 'var(--text-light, #f9fafb)' : 'var(--text-dark, #1f2937)'};\nborder-radius: 12px;\noverflow: hidden;\nbox-shadow: var(--shadow, 0 4px 6px -1px rgba(0, 0, 0, 0.1));\ntransition: all 0.3s ease;\n}\n.profile-header {\ndisplay: flex;\nalign-items: center;\npadding: 24px;\nbackground-color: ${isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.02)'};\nborder-bottom: 1px solid ${isDark ? 'var(--border-dark, #4b5563)' : 'var(--border-light, #e5e7eb)'};\n}\n.avatar-container {\nmargin-right: 20px;\n}\n.info-container {\nflex: 1;\n}\n.stats-container {\ndisplay: flex;\njustify-content: space-between;\npadding: 16px 24px;\nborder-top: 1px solid ${isDark ? 'var(--border-dark, #4b5563)' : 'var(--border-light, #e5e7eb)'};\n}\n/* Empty slot styling */\n.avatar-container:empty::before,\n.info-container:empty::before,\n.stats-container:empty::before {\ndisplay: block;\npadding: 16px;\nbackground-color: ${isDark ? 'rgba(255, 255, 255, 0.05)' : 'rgba(0, 0, 0, 0.03)'};\nborder-radius: 6px;\nfont-style: italic;\nfont-size: 14px;\ncolor: ${isDark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.4)'};\n}\n.avatar-container:empty::before {\ncontent: 'No avatar content provided';\nwidth: 80px;\nheight: 80px;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\ntext-align: center;\nborder-radius: 50%;\n}\n.info-container:empty::before {\ncontent: 'No user info provided';\n}\n.stats-container:empty::before {\ncontent: 'No stats content provided';\n}\n</style>\n<div class=\"profile\">\n<div class=\"profile-header\">\n<div class=\"avatar-container\">\n<slot name=\"avatar\"></slot>\n</div>\n<div class=\"info-container\">\n<slot name=\"info\"></slot>\n</div>\n</div>\n<div class=\"stats-container\">\n<slot name=\"stats\"></slot>\n</div>\n</div>\n`;\n// Set up slot change listeners\nthis.shadowRoot.querySelectorAll('slot').forEach(slot => {\nslot.addEventListener('slotchange', () => this.handleSlotChange(slot));\n});\n// Update code preview\nupdateCodePreview();\n}\n// Handle slot content changes\nhandleSlotChange(slot) {\nconst slotName = slot.name;\nconst hasContent = slot.assignedNodes().length > 0;\n// Update the diagram to reflect slot status\nconst indicator = document.getElementById(`${slotName}-indicator`);\nif (indicator) {\nconst slotElement = indicator.closest('.slot');\nif (hasContent) {\nslotElement.classList.remove('empty');\nindicator.textContent = 'Content from Light DOM';\nindicator.classList.add('highlight');\nsetTimeout(() => indicator.classList.remove('highlight'), 1000);\n} else {\nslotElement.classList.add('empty');\nindicator.textContent = 'Empty slot (no content provided)';\n}\n}\n}}\n// Register the custom element\ncustomElements.define('user-profile', UserProfile);\n// Function to update code preview\nfunction updateCodePreview() {\nconst avatarVisible = document.getElementById('avatar-content').style.display !== 'none';\nconst infoVisible = document.getElementById('info-content').style.display !== 'none';\nconst statsVisible = document.getElementById('stats-content').style.display !== 'none';\nconst profileElement = document.querySelector('user-profile');\nconst theme = profileElement.getAttribute('theme') || 'light';\nlet code = `<!-- Container component -->\\n<user-profile theme=\"${theme}\">\\n`;\n// Add avatar section if visible\nif (avatarVisible) {\ncode += `  <!-- Composable child component for avatar -->\\n`;\ncode += `  <div slot=\"avatar\">\\n`;\ncode += `    <img src=\"https://randomuser.me/api/portraits/women/42.jpg\" alt=\"User avatar\">\\n`;\ncode += `  </div>\\n\\n`;\n}\n// Add info section if visible\nif (infoVisible) {\ncode += `  <!-- Composable child component for user info -->\\n`;\ncode += `  <div slot=\"info\">\\n`;\ncode += `    <h3>Sarah Johnson</h3>\\n`;\ncode += `    <p class=\"job-title\">Senior Web Developer</p>\\n`;\ncode += `    <p class=\"location\">San Francisco, CA</p>\\n`;\ncode += `  </div>\\n\\n`;\n}\n// Add stats section if visible\nif (statsVisible) {\ncode += `  <!-- Composable child component for stats -->\\n`;\ncode += `  <div slot=\"stats\">\\n`;\ncode += `    <div class=\"stat-item\">\\n`;\ncode += `      <span class=\"stat-value\">142</span>\\n`;\ncode += `      <span class=\"stat-label\">Projects</span>\\n`;\ncode += `    </div>\\n`;\ncode += `    <div class=\"stat-item\">\\n`;\ncode += `      <span class=\"stat-value\">3.2K</span>\\n`;\ncode += `      <span class=\"stat-label\">Followers</span>\\n`;\ncode += `    </div>\\n`;\ncode += `    <div class=\"stat-item\">\\n`;\ncode += `      <span class=\"stat-value\">87</span>\\n`;\ncode += `      <span class=\"stat-label\">Reviews</span>\\n`;\ncode += `    </div>\\n`;\ncode += `  </div>\\n`;\n}\ncode += `</user-profile>`;\n// Update the code display\ndocument.getElementById('code-output').textContent = code;\n}\n// Set up the UI controls\ndocument.addEventListener('DOMContentLoaded', () => {\nconst avatarToggle = document.getElementById('toggle-avatar');\nconst infoToggle = document.getElementById('toggle-info');\nconst statsToggle = document.getElementById('toggle-stats');\nconst themeToggle = document.getElementById('toggle-theme');\nconst avatarContent = document.getElementById('avatar-content');\nconst infoContent = document.getElementById('info-content');\nconst statsContent = document.getElementById('stats-content');\nconst profileElement = document.querySelector('user-profile');\n// Toggle avatar content\navatarToggle.addEventListener('click', () => {\navatarToggle.classList.toggle('active');\nif (avatarToggle.classList.contains('active')) {\navatarContent.style.display = '';\n} else {\navatarContent.style.display = 'none';\n}\nupdateCodePreview();\n});\n// Toggle info content\ninfoToggle.addEventListener('click', () => {\ninfoToggle.classList.toggle('active');\nif (infoToggle.classList.contains('active')) {\ninfoContent.style.display = '';\n} else {\ninfoContent.style.display = 'none';\n}\nupdateCodePreview();\n});\n// Toggle stats content\nstatsToggle.addEventListener('click', () => {\nstatsToggle.classList.toggle('active');\nif (statsToggle.classList.contains('active')) {\nstatsContent.style.display = '';\n} else {\nstatsContent.style.display = 'none';\n}\nupdateCodePreview();\n});\n// Toggle theme\nthemeToggle.addEventListener('click', () => {\nthemeToggle.classList.toggle('active');\nconst newTheme = profileElement.getAttribute('theme') === 'light' ? 'dark' : 'light';\nprofileElement.setAttribute('theme', newTheme);\nif (newTheme === 'dark') {\nthemeToggle.textContent = 'Switch to Light';\n} else {\nthemeToggle.textContent = 'Switch to Dark';\n}\nupdateCodePreview();\n});\n// Initialize the component\nsetTimeout(() => {\n// Trigger slot change events\nprofileElement.shadowRoot.querySelectorAll('slot').forEach(slot => {\nconst event = new Event('slotchange');\nslot.dispatchEvent(event);\n});\n// Initialize code preview\nupdateCodePreview();\n}, 100);\n});\nWeb Component Composition Pattern Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis card component uses three distinct slots (header, body, and footer) to create a flexible, reusable UI element. Try toggling different parts of the content to see how:\nThe component maintains its structure even when slots are empty\nContent from the \"light DOM\" (your HTML) flows into the designated slots\nThe component's styling remains consistent regardless of the content provided\nThe theme can change while preserving the composition structure\nThe code preview updates to show exactly what HTML you would write to achieve the current state\nThis pattern is especially powerful for design systems, as it allows components to maintain consistent styling and behavior while giving content authors flexibility to provide custom content for different sections.\nProperty/Attribute Pattern\nAttributes for simple values, properties for complex data:\nclass DataTable extends HTMLElement {\n// Simple configuration via attributes\nstatic get observedAttributes() {\nreturn ['loading', 'pagination'];\n}\n// Complex data via properties\nset data(value) {\nthis._data = value;\nthis._renderTable();\n}\n}\nAn attribute is the text you write in HTML (<my-el size=\"large\">), and it is always a string. A property is a field on the element object in JavaScript (el.items = [1, 2, 3]), and it can hold anything: arrays, objects, functions. Use attributes for small settings you want visible in markup, and properties for real data.\nThe failure first: ask what an attribute does to an array.\nel.setAttribute('items', ['a', 'b']);\nel.getAttribute('items'); // \"a,b\"  (one string, not an array)\nel.items = ['a', 'b'];     // the array itself, if the component has an items property\nTo better understand how attributes and properties work together in Web Components, let's explore this interactive demo that clearly demonstrates their differences:\nHTML\n<div class=\"container\">\n<h2>Property vs. Attribute Pattern Demo</h2>\n<div class=\"explanation\">\n<p><strong>Attributes</strong> are for simple values (strings, booleans) and appear in HTML markup. They can be observed with <code>attributeChangedCallback()</code>.</p>\n<p><strong>Properties</strong> are for complex data (objects, arrays) and are set programmatically. They're not reflected in the HTML markup.</p>\n</div>\n<div class=\"demo-section\">\n<div class=\"controls-section\">\n<div class=\"control-group\">\n<h3>Attributes (Simple Values)</h3>\n<div class=\"control\">\n<label>Theme:</label>\n<select id=\"theme-select\">\n<option value=\"light\">Light</option>\n<option value=\"dark\">Dark</option>\n<option value=\"colorful\">Colorful</option>\n</select>\n</div>\n<div class=\"control\">\n<label>Size:</label>\n<select id=\"size-select\">\n<option value=\"small\">Small</option>\n<option value=\"medium\">Medium</option>\n<option value=\"large\">Large</option>\n</select>\n</div>\n<div class=\"control\">\n<label>Disabled:</label>\n<input type=\"checkbox\" id=\"disabled-toggle\">\n</div>\n</div>\n<div class=\"control-group\">\n<h3>Properties (Complex Data)</h3>\n<div class=\"control\">\n<label>Set Items (Array):</label>\n<select id=\"items-preset\">\n<option value=\"fruits\">Fruits</option>\n<option value=\"colors\">Colors</option>\n<option value=\"countries\">Countries</option>\n</select>\n<button id=\"set-items\">Set Items Array</button>\n</div>\n<div class=\"control\">\n<label>Set Config (Object):</label>\n<select id=\"config-preset\">\n<option value=\"default\">Default Config</option>\n<option value=\"compact\">Compact Config</option>\n<option value=\"expanded\">Expanded Config</option>\n</select>\n<button id=\"set-config\">Set Config Object</button>\n</div>\n</div>\n</div>\n<div class=\"component-container\">\n<h3>Component Output</h3>\n<data-display-component\ntheme=\"light\"\nsize=\"medium\">\n</data-display-component>\n</div>\n</div>\n<div class=\"state-display\">\n<div class=\"state-section\">\n<h3>Current HTML Markup</h3>\n<pre id=\"html-display\"></pre>\n</div>\n<div class=\"state-section\">\n<h3>Current Properties</h3>\n<pre id=\"props-display\"></pre>\n</div>\n<div class=\"state-section\">\n<h3>Component Events Log</h3>\n<div id=\"events-log\"></div>\n<button id=\"clear-log\">Clear Log</button>\n</div>\n</div>\n</div>\nCSS\n.container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 900px;\nmargin: 0 auto;\npadding: 20px;\n}\n.explanation {\nbackground-color: #f0f4f8;\npadding: 15px;\nborder-radius: 6px;\nmargin-bottom: 20px;\nborder-left: 4px solid #4a6cf7;\n}\n.demo-section {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\nmargin-bottom: 20px;\n}\n.controls-section {\nflex: 1;\nmin-width: 300px;\n}\n.component-container {\nflex: 1;\nmin-width: 300px;\npadding: 15px;\nborder: 2px dashed #ccc;\nborder-radius: 6px;\n}\n.control-group {\nbackground-color: #f8f9fa;\npadding: 15px;\nborder-radius: 6px;\nmargin-bottom: 15px;\n}\n.control-group h3 {\nmargin-top: 0;\ncolor: #4a6cf7;\nfont-weight: 500;\n}\n.control {\nmargin-bottom: 12px;\ndisplay: flex;\nalign-items: center;\ngap: 10px;\n}\n.control label {\nmin-width: 120px;\nfont-weight: 500;\n}\nbutton {\npadding: 8px 12px;\nbackground-color: #4a6cf7;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 14px;\n}\nbutton:hover {\nbackground-color: #3a5cf7;\n}\nselect, input {\npadding: 8px;\nborder: 1px solid #ccc;\nborder-radius: 4px;\n}\n.state-display {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\nmargin-top: 20px;\n}\n.state-section {\nflex: 1;\nmin-width: 280px;\nbackground-color: #f8f9fa;\npadding: 15px;\nborder-radius: 6px;\n}\n.state-section h3 {\nmargin-top: 0;\ncolor: #4a6cf7;\nfont-weight: 500;\n}\npre {\nbackground-color: #f1f1f1;\npadding: 10px;\nborder-radius: 4px;\noverflow-x: auto;\nfont-family: monospace;\nmargin: 0;\n}\n#events-log {\nheight: 150px;\noverflow-y: auto;\nbackground-color: #f1f1f1;\npadding: 10px;\nborder-radius: 4px;\nfont-family: monospace;\nmargin-bottom: 10px;\n}\n.log-entry {\nmargin-bottom: 5px;\npadding-bottom: 5px;\nborder-bottom: 1px solid #ddd;\n}\n.log-attribute {\ncolor: #e63946;\n}\n.log-property {\ncolor: #2a9d8f;\n}\n#clear-log {\nbackground-color: #6c757d;\n}\n/* Highlight the attributes vs. properties sections */\n.control-group:nth-child(1) {\nborder-left: 4px solid #e63946; /* Red for attributes */\n}\n.control-group:nth-child(2) {\nborder-left: 4px solid #2a9d8f; /* Green for properties */\n}\nJavaScript\n// Define the custom component\nclass DataDisplayComponent extends HTMLElement {\nconstructor() {\nsuper();\n// Initialize private properties\nthis._items = [];\nthis._config = {\nshowHeaders: true,\nmaxItems: 5,\nanimation: 'fade' };\n// Create a shadow root\nthis.attachShadow({ mode: 'open' });\n// Do initial render\nthis.render();\n// Log the construction\nthis.logEvent('constructor', 'Component created');\n}\n// Specify which attributes should be observed\nstatic get observedAttributes() {\nreturn ['theme', 'size', 'disabled'];\n}\n// Lifecycle: when component is added to DOM\nconnectedCallback() {\nthis.logEvent('connected', 'Component added to DOM');\nthis.updateDisplay();\n}\n// Lifecycle: when observed attributes change\nattributeChangedCallback(name, oldValue, newValue) {\nthis.logEvent('attribute-changed', `Attribute '${name}' changed from '${oldValue}' to '${newValue}'`);\nthis.render();\n}\n// PROPERTY for items (array)\nget items() {\nreturn this._items;\n}\nset items(newItems) {\nthis.logEvent('property-changed', 'Property \"items\" set with array data');\nthis._items = newItems;\nthis.render();\n}\n// PROPERTY for config (object)\nget config() {\nreturn this._config;\n}\nset config(newConfig) {\nthis.logEvent('property-changed', 'Property \"config\" set with object data');\nthis._config = { ...this._config, ...newConfig };\nthis.render();\n}\n// Helper for rendering the component\nrender() {\n// Get attribute values\nconst theme = this.getAttribute('theme') || 'light';\nconst size = this.getAttribute('size') || 'medium';\nconst disabled = this.hasAttribute('disabled');\n// Prepare styles based on attributes\nconst themeColors = {\nlight: { bg: '#ffffff', text: '#333333', border: '#dddddd' },\ndark: { bg: '#333333', text: '#ffffff', border: '#555555' },\ncolorful: { bg: '#f8f0ff', text: '#4a0072', border: '#9c27b0' } };\nconst sizeValues = {\nsmall: { padding: '10px', fontSize: '14px' },\nmedium: { padding: '15px', fontSize: '16px' },\nlarge: { padding: '20px', fontSize: '18px' } };\nconst colors = themeColors[theme] || themeColors.light;\nconst sizes = sizeValues[size] || sizeValues.medium;\n// Generate the Shadow DOM content\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: inherit;\n}\n.component {\nbackground-color: ${colors.bg};\ncolor: ${colors.text};\nborder: 1px solid ${colors.border};\nborder-radius: 8px;\npadding: ${sizes.padding};\nfont-size: ${sizes.fontSize};\nopacity: ${disabled ? '0.6' : '1'};\n${disabled ? 'pointer-events: none;' : ''}\ntransition: all 0.3s ease;\n}\n.info-section {\nmargin-bottom: 10px;\n}\n.data-section {\nborder-top: 1px solid ${colors.border};\npadding-top: 10px;\nmargin-top: 10px;\n}\nul {\nmargin: 0;\npadding-left: 20px;\n}\nli {\nmargin-bottom: 5px;\n}\n.config-info {\nfont-family: monospace;\npadding: 5px;\nbackground: rgba(0,0,0,0.05);\nborder-radius: 4px;\n}\n</style>\n<div class=\"component\">\n<div class=\"info-section\">\n<strong>Component States:</strong>\n<div>Theme: ${theme}</div>\n<div>Size: ${size}</div>\n<div>Disabled: ${disabled ? 'Yes' : 'No'}</div>\n</div>\n${this._items.length ? `\n<div class=\"data-section\">\n${this._config.showHeaders ? '<strong>Items (from property):</strong>' : ''}\n<ul>\n${this._items.slice(0, this._config.maxItems).map(item => `<li>${item}</li>`).join('')}\n</ul>\n${this._items.length > this._config.maxItems ? `<small>Showing ${this._config.maxItems} of ${this._items.length} items...</small>` : ''}\n</div>\n` : ''}\n<div class=\"data-section\">\n<strong>Config (from property):</strong>\n<div class=\"config-info\">\nshowHeaders: ${this._config.showHeaders}<br>\nmaxItems: ${this._config.maxItems}<br>\nanimation: ${this._config.animation}\n</div>\n</div>\n</div>\n`;\n// Update the state displays\nthis.updateDisplay();\n}\n// Helper for updating the display of HTML and properties\nupdateDisplay() {\nconst htmlDisplay = document.getElementById('html-display');\nconst propsDisplay = document.getElementById('props-display');\nif (htmlDisplay) {\n// Generate the current HTML representation\nconst html = `<data-display-component\ntheme=\"${this.getAttribute('theme') || 'light'}\"\nsize=\"${this.getAttribute('size') || 'medium'}\"${this.hasAttribute('disabled') ? '\\n  disabled' : ''}>\n</data-display-component>`;\nhtmlDisplay.textContent = html;\n}\nif (propsDisplay) {\n// Generate the current properties representation\nconst props = {\nitems: this._items,\nconfig: this._config };\npropsDisplay.textContent = JSON.stringify(props, null, 2);\n}\n}\n// Helper for logging events\nlogEvent(type, message) {\nconst logContainer = document.getElementById('events-log');\nif (logContainer) {\nconst entry = document.createElement('div');\nentry.className = `log-entry log-${type.includes('attribute') ? 'attribute' : 'property'}`;\nentry.textContent = `${new Date().toLocaleTimeString()} - ${message}`;\nlogContainer.appendChild(entry);\nlogContainer.scrollTop = logContainer.scrollHeight;\n}\n}}\n// Register the custom element\ncustomElements.define('data-display-component', DataDisplayComponent);\n// Setup the demo controls\ndocument.addEventListener('DOMContentLoaded', () => {\nconst component = document.querySelector('data-display-component');\n// Attribute controls\ndocument.getElementById('theme-select').addEventListener('change', e => {\ncomponent.setAttribute('theme', e.target.value);\n});\ndocument.getElementById('size-select').addEventListener('change', e => {\ncomponent.setAttribute('size', e.target.value);\n});\ndocument.getElementById('disabled-toggle').addEventListener('change', e => {\nif (e.target.checked) {\ncomponent.setAttribute('disabled', '');\n} else {\ncomponent.removeAttribute('disabled');\n}\n});\n// Property controls for items\ndocument.getElementById('set-items').addEventListener('click', () => {\nconst preset = document.getElementById('items-preset').value;\nlet items = [];\nswitch (preset) {\ncase 'fruits':\nitems = ['Apple', 'Banana', 'Cherry', 'Dragon Fruit', 'Elderberry', 'Fig', 'Grapefruit'];\nbreak;\ncase 'colors':\nitems = ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Indigo', 'Violet'];\nbreak;\ncase 'countries':\nitems = ['Australia', 'Brazil', 'Canada', 'Denmark', 'Egypt', 'France', 'Germany'];\nbreak;}\n// Set the property\ncomponent.items = items;\n});\n// Property controls for config\ndocument.getElementById('set-config').addEventListener('click', () => {\nconst preset = document.getElementById('config-preset').value;\nlet config = {};\nswitch (preset) {\ncase 'default':\nconfig = { showHeaders: true, maxItems: 5, animation: 'fade' };\nbreak;\ncase 'compact':\nconfig = { showHeaders: false, maxItems: 3, animation: 'none' };\nbreak;\ncase 'expanded':\nconfig = { showHeaders: true, maxItems: 10, animation: 'slide' };\nbreak;}\n// Set the property\ncomponent.config = config;\n});\n// Clear log button\ndocument.getElementById('clear-log').addEventListener('click', () => {\ndocument.getElementById('events-log').innerHTML = '';\n});\n// Initialize displays\ncomponent.updateDisplay();\n});\nProperty/Attribute Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis demo shows how attributes are best for simple values that can be represented in HTML markup (strings, booleans), while properties excel at handling complex data like arrays and objects. Try using the controls to manipulate both attributes and properties, and observe how:\nAttribute changes trigger the attributeChangedCallback() and appear in the HTML markup\nProperty changes don't affect the HTML markup but can handle complex data structures\nThe component responds to both types of changes in different ways\nThis split is common in published libraries: small options such as a size or a disabled flag are attributes, and structured data is passed as a property.\nQuestion: You call el.setAttribute('items', ['a', 'b']). What does el.getAttribute('items') return?\nThe array ['a', 'b']\nThe string \"a,b\"\nAn object with two entries\nAnswer: 2. The string \"a,b\"\nExplanation: Attributes only hold strings, so the array is converted with toString(). To hand the component a real array, set a property: el.items = ['a', 'b'].\nCoding challenge: Attribute or property?\nPass the same three fruits to <item-list> twice and compare what the component receives.\nExpected result: after you finish step 1 the preview says the property is an array of 3 items; after step 2 the attribute line shows one string, apple,pear,plum (15 characters), and the property line is unchanged.\nHTML\n<item-list id=\"list\"></item-list>\nJavaScript\nclass ItemList extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._items = null;\n}\nset items(value) { this._items = value; this.render(); }\nget items() { return this._items; }\nstatic get observedAttributes() { return ['items']; }\nattributeChangedCallback() { this.render(); }\nconnectedCallback() { this.render(); }\nrender() {\nconst attr = this.getAttribute('items');\nthis.shadowRoot.innerHTML =\n'<p>property: ' + (Array.isArray(this._items) ? 'an array of ' + this._items.length : 'not set') + '</p>' +\n'<p>attribute: ' + (attr === null ? 'not set' : 'the string \"' + attr + '\" (' + attr.length + ' characters)') + '</p>';\n}\n}\ncustomElements.define('item-list', ItemList);\nconst el = document.getElementById('list');\n// TODO 1: set the property to ['apple', 'pear', 'plum'].\n// TODO 2: now set the attribute 'items' to the same array with setAttribute.\nPass an array: property vs attribute on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nTip:\nStep 1 is el.items = [...]. Step 2 is el.setAttribute('items', [...]). Notice the attribute line counts characters, not fruits.\nEvent-Based Communication Pattern\nComponents communicate through custom events:\n// Inside the component\nthis.dispatchEvent(new CustomEvent('item-selected', {\nbubbles: true,\ncomposed: true, // Crosses shadow DOM boundary\ndetail: { id: selectedId }\n}));\n// Parent listening for events\ndocument.querySelector('my-component')\n.addEventListener('item-selected', (e) => console.log(e.detail.id));\nMany small custom elements in the wild, such as the open-source <relative-time> element, communicate this way.\nQuestion: A custom event is dispatched inside a component's shadow root. A normal (non-capturing) listener on document must receive it. Which options does the event need?\nbubbles: true only\ncomposed: true only\nbubbles: true and composed: true\nAnswer: 3. bubbles: true and composed: true\nExplanation: bubbles lets the event travel up through ancestors; composed lets it leave the shadow root. Without both, the event stops inside the shadow tree or never goes up.\nLet's see events in action with this interactive demo. Here, two completely separate web components communicate through custom events without direct references to each other - a fundamental pattern for creating loosely coupled, reusable components:\nHTML\n<div class=\"container\">\n<h2>Web Component Event Communication Demo</h2>\n<div class=\"explanation\">\n<p>This demo shows how Web Components can communicate using custom events. The color picker component dispatches events that the display component listens for.</p>\n</div>\n<div class=\"component-wrapper\">\n<div class=\"component-section\">\n<h3>Component 1: Color Picker</h3>\n<color-picker-component></color-picker-component>\n</div>\n<div class=\"event-flow\">\n<div class=\"event-arrow\">\n<div class=\"arrow-line\"></div>\n<div class=\"arrow-head\"></div>\n</div>\n<div class=\"event-info\">\n<code>color-changed</code> event\n<div class=\"event-detail\">detail: { color, name }</div>\n</div>\n</div>\n<div class=\"component-section\">\n<h3>Component 2: Display</h3>\n<color-display-component></color-display-component>\n</div>\n</div>\n<div class=\"event-log-section\">\n<h3>Event Communication Log</h3>\n<div id=\"event-log\"></div>\n<button id=\"clear-log\">Clear Log</button>\n</div>\n</div>\nCSS\n.container {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.explanation {\nbackground-color: #f0f8ff;\npadding: 15px;\nborder-radius: 6px;\nmargin-bottom: 20px;\nborder-left: 4px solid #4a6cf7;\n}\n.component-wrapper {\ndisplay: flex;\nalign-items: center;\njustify-content: space-between;\nmargin: 30px 0;\ngap: 20px;\n}\n.component-section {\nflex: 1;\nmin-width: 200px;\nbackground-color: #f8f9fa;\npadding: 20px;\nborder-radius: 8px;\nbox-shadow: 0 2px 5px rgba(0,0,0,0.1);\n}\n.component-section h3 {\nmargin-top: 0;\ncolor: #333;\nfont-size: 18px;\nborder-bottom: 1px solid #ddd;\npadding-bottom: 10px;\nmargin-bottom: 15px;\n}\n.event-flow {\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\npadding: 0 10px;\n}\n.event-arrow {\nposition: relative;\nwidth: 100%;\nheight: 40px;\n}\n.arrow-line {\nposition: absolute;\ntop: 50%;\nleft: 0;\nright: 0;\nheight: 2px;\nbackground-color: #4a6cf7;\ntransform: translateY(-50%);\n}\n.arrow-head {\nposition: absolute;\ntop: 50%;\nright: 0;\nwidth: 0;\nheight: 0;\nborder-top: 8px solid transparent;\nborder-bottom: 8px solid transparent;\nborder-left: 10px solid #4a6cf7;\ntransform: translateY(-50%);\n}\n.event-info {\nbackground-color: #e8f0fe;\nborder: 1px solid #4a6cf7;\nborder-radius: 4px;\npadding: 6px 10px;\nfont-size: 14px;\nmargin-top: 5px;\ntext-align: center;\n}\n.event-detail {\nfont-family: monospace;\nfont-size: 12px;\ncolor: #666;\nmargin-top: 5px;\n}\n.event-log-section {\nbackground-color: #f8f9fa;\npadding: 20px;\nborder-radius: 8px;\nmargin-top: 30px;\n}\n.event-log-section h3 {\nmargin-top: 0;\ncolor: #333;\n}\n#event-log {\nbackground-color: #f1f1f1;\nborder: 1px solid #ddd;\nborder-radius: 4px;\npadding: 10px;\nheight: 200px;\noverflow-y: auto;\nfont-family: monospace;\nmargin-bottom: 10px;\n}\n.log-entry {\nmargin-bottom: 8px;\npadding-bottom: 8px;\nborder-bottom: 1px solid #ddd;\n}\n.log-entry-time {\ncolor: #666;\nfont-size: 12px;\n}\n.log-entry-message {\nmargin-top: 3px;\n}\n.log-highlight {\nbackground-color: #fffbe6;\npadding: 2px 4px;\nborder-radius: 3px;\n}\nbutton {\npadding: 8px 12px;\nbackground-color: #4a6cf7;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 14px;\n}\nbutton:hover {\nbackground-color: #3a5cf7;\n}\n#clear-log {\nbackground-color: #6c757d;\n}\n/* Animation for events */\n@keyframes pulse {\n0% { transform: scale(1); }\n50% { transform: scale(1.05); }\n100% { transform: scale(1); }\n}\n.event-pulse {\nanimation: pulse 0.5s ease-in-out;\n}\nJavaScript\n// Helper function to log events\nfunction logEvent(message, highlight = false) {\nconst logContainer = document.getElementById('event-log');\nconst entry = document.createElement('div');\nentry.className = 'log-entry';\nconst time = document.createElement('div');\ntime.className = 'log-entry-time';\ntime.textContent = new Date().toLocaleTimeString();\nconst messageEl = document.createElement('div');\nmessageEl.className = 'log-entry-message';\nif (highlight) {\nmessageEl.innerHTML = `<span class=\"log-highlight\">${message}</span>`;\n} else {\nmessageEl.textContent = message;\n}\nentry.appendChild(time);\nentry.appendChild(messageEl);\nlogContainer.appendChild(entry);\nlogContainer.scrollTop = logContainer.scrollHeight;\n}\n// Define the Color Picker Component\nclass ColorPickerComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Color options with names\nthis.colors = [\n{ hex: '#FF5252', name: 'Coral Red' },\n{ hex: '#FFD740', name: 'Amber' },\n{ hex: '#69F0AE', name: 'Mint Green' },\n{ hex: '#40C4FF', name: 'Sky Blue' },\n{ hex: '#B388FF', name: 'Lavender' },\n{ hex: '#FF80AB', name: 'Pink' }];\nthis.render();\nlogEvent('Color Picker component initialized');\n}\nconnectedCallback() {\n// Add event listeners to color swatches\nconst swatches = this.shadowRoot.querySelectorAll('.color-swatch');\nswatches.forEach(swatch => {\nswatch.addEventListener('click', this.handleColorSelect.bind(this));\n});\nlogEvent('Color Picker connected to DOM');\n}\nhandleColorSelect(event) {\nconst color = event.target.dataset.color;\nconst name = event.target.dataset.name;\n// Highlight the selected swatch\nconst swatches = this.shadowRoot.querySelectorAll('.color-swatch');\nswatches.forEach(swatch => {\nif (swatch.dataset.color === color) {\nswatch.classList.add('selected');\n} else {\nswatch.classList.remove('selected');\n}\n});\n// Log internal state change\nlogEvent(`Color selected: ${name} (${color})`);\n// Apply visual feedback\nconst container = this.shadowRoot.querySelector('.picker-container');\ncontainer.classList.add('event-pulse');\nsetTimeout(() => container.classList.remove('event-pulse'), 500);\n// Dispatch custom event\nthis.dispatchEvent(new CustomEvent('color-changed', {\nbubbles: true, // Event bubbles up through the DOM\ncomposed: true, // Event crosses the shadow DOM boundary\ndetail: { // Data to send with the event\ncolor: color,\nname: name } }));\n// Log event dispatch\nlogEvent(`Event dispatched: color-changed with color=${color}, name=${name}`, true);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: inherit;\n}\n.picker-container {\nbackground-color: #fff;\nborder-radius: 8px;\npadding: 15px;\nbox-shadow: 0 2px 10px rgba(0,0,0,0.05);\ntransition: all 0.3s ease;\n}\n.color-palette {\ndisplay: grid;\ngrid-template-columns: repeat(3, 1fr);\ngap: 10px;\nmargin-top: 10px;\n}\n.color-swatch {\nheight: 50px;\nborder-radius: 6px;\ncursor: pointer;\ntransition: transform 0.2s ease, box-shadow 0.2s ease;\nposition: relative;\n}\n.color-swatch:hover {\ntransform: translateY(-2px);\nbox-shadow: 0 4px 8px rgba(0,0,0,0.1);\n}\n.color-swatch.selected {\nbox-shadow: 0 0 0 3px #333;\n}\n.description {\nmargin-bottom: 15px;\nfont-size: 14px;\ncolor: #666;\n}\n</style>\n<div class=\"picker-container\">\n<div class=\"description\">\nSelect a color to send an event to the display component\n</div>\n<div class=\"color-palette\">\n${this.colors.map(color => `\n<div class=\"color-swatch\"\nstyle=\"background-color: ${color.hex}\"\ndata-color=\"${color.hex}\"\ndata-name=\"${color.name}\">\n</div>\n`).join('')}\n</div>\n</div>\n`;\n}}\n// Define the Color Display Component\nclass ColorDisplayComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Default state\nthis.currentColor = '#EEEEEE';\nthis.colorName = 'None Selected';\nthis.render();\nlogEvent('Color Display component initialized');\n}\nconnectedCallback() {\n// Listen for color-changed events\ndocument.addEventListener('color-changed', this.handleColorChange.bind(this));\nlogEvent('Color Display connected to DOM');\nlogEvent('Event listener added for \"color-changed\" events');\n}\ndisconnectedCallback() {\n// Clean up event listeners when component is removed\ndocument.removeEventListener('color-changed', this.handleColorChange.bind(this));\nlogEvent('Color Display removed from DOM');\nlogEvent('Event listener removed for \"color-changed\" events');\n}\nhandleColorChange(event) {\n// Extract color data from the event\nconst { color, name } = event.detail;\n// Update internal state\nthis.currentColor = color;\nthis.colorName = name;\n// Log event reception\nlogEvent(`Event received: color-changed with color=${color}, name=${name}`, true);\n// Apply visual feedback\nconst container = this.shadowRoot.querySelector('.display-container');\ncontainer.classList.add('event-pulse');\nsetTimeout(() => container.classList.remove('event-pulse'), 500);\n// Update the component\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: inherit;\n}\n.display-container {\nbackground-color: #fff;\nborder-radius: 8px;\noverflow: hidden;\nbox-shadow: 0 2px 10px rgba(0,0,0,0.05);\ntransition: all 0.3s ease;\n}\n.color-preview {\nheight: 130px;\nbackground-color: ${this.currentColor};\ntransition: background-color 0.3s ease;\n}\n.color-info {\npadding: 15px;\n}\n.color-name {\nfont-size: 18px;\nfont-weight: 500;\nmargin-bottom: 5px;\n}\n.color-hex {\nfont-family: monospace;\ncolor: #666;\nfont-size: 14px;\n}\n.help-text {\nmargin-top: 10px;\nfont-size: 12px;\ncolor: #999;\nfont-style: italic;\n}\n</style>\n<div class=\"display-container\">\n<div class=\"color-preview\"></div>\n<div class=\"color-info\">\n<div class=\"color-name\">${this.colorName}</div>\n<div class=\"color-hex\">${this.currentColor}</div>\n<div class=\"help-text\">This component listens for color-changed events</div>\n</div>\n</div>\n`;\n}}\n// Register the custom elements\ncustomElements.define('color-picker-component', ColorPickerComponent);\ncustomElements.define('color-display-component', ColorDisplayComponent);\n// Setup clear log button\ndocument.addEventListener('DOMContentLoaded', () => {\ndocument.getElementById('clear-log').addEventListener('click', () => {\ndocument.getElementById('event-log').innerHTML = '';\nlogEvent('Log cleared');\n});\n// Initial log message\nlogEvent('Demo initialized');\nlogEvent('Click on a color in the Color Picker to see event communication in action');\n});\n// Visual effects for the arrow when events happen\ndocument.addEventListener('color-changed', () => {\nconst arrow = document.querySelector('.event-arrow');\narrow.style.opacity = '0.2';\nsetTimeout(() => {\narrow.style.opacity = '1';\n}, 50);\n});\nEvent Communication Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nIn this example:\nThe color picker component dispatches a color-changed event when a color is selected\nThe event includes detailed data in the detail property (color code and name)\nThe display component listens for this event and updates accordingly\nNotice how bubbles: true and composed: true allow the event to cross shadow DOM boundaries\nAPI Consistency Pattern\nSuccessful Web Component libraries establish consistent APIs:\n<!-- Consistent naming and behavior across components -->\n<sl-button variant=\"primary\" size=\"large\" disabled></sl-button>\n<sl-dropdown variant=\"primary\" size=\"large\" disabled></sl-dropdown>\n<sl-checkbox variant=\"primary\" size=\"large\" disabled></sl-checkbox>\nShoelace, for example, uses shared names like variant, size and disabled across its components.\nReal-World Implementation Examples\nDesign System Components\nDesign systems often implement form controls with enhanced functionality:\nclass EnhancedSelect extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: inline-block;\nposition: relative;\nwidth: 200px;\n}\n.select-container {\nborder: 1px solid var(--border-color, #ccc);\nborder-radius: 4px;\npadding: 8px;\n}\n/* Additional styles... */\n</style>\n<div class=\"select-container\">\n<slot name=\"selected-option\"></slot>\n<div class=\"dropdown\">\n<slot></slot>\n</div>\n</div>\n`;\n// Implementation details...\n}\n}\ncustomElements.define('enhanced-select', EnhancedSelect);\nNow let's explore a fully functional implementation of the EnhancedSelect component that demonstrates how design system principles are applied in practice:\nHTML\n<div class=\"container\">\n<h2>Design System Component Demo</h2>\n<div class=\"intro\">\n<p>This enhanced select component demonstrates design system principles including theming, consistent styling, and accessibility.</p>\n</div>\n<div class=\"demo-controls\">\n<div class=\"control-group\">\n<h3>Design System Theming</h3>\n<div class=\"control\">\n<label for=\"theme-selector\">Theme:</label>\n<select id=\"theme-selector\">\n<option value=\"light\">Light</option>\n<option value=\"dark\">Dark</option>\n<option value=\"brand\">Brand</option>\n</select>\n</div>\n<div class=\"control\">\n<label for=\"density-selector\">Density:</label>\n<select id=\"density-selector\">\n<option value=\"comfortable\">Comfortable</option>\n<option value=\"compact\">Compact</option>\n</select>\n</div>\n<div class=\"control checkbox\">\n<input type=\"checkbox\" id=\"rounded-toggle\" checked>\n<label for=\"rounded-toggle\">Rounded Corners</label>\n</div>\n</div>\n<div class=\"control-group\">\n<h3>Component State</h3>\n<div class=\"control checkbox\">\n<input type=\"checkbox\" id=\"disabled-toggle\">\n<label for=\"disabled-toggle\">Disabled</label>\n</div>\n<div class=\"control checkbox\">\n<input type=\"checkbox\" id=\"required-toggle\">\n<label for=\"required-toggle\">Required</label>\n</div>\n<div class=\"control\">\n<label for=\"size-selector\">Size:</label>\n<select id=\"size-selector\">\n<option value=\"small\">Small</option>\n<option value=\"medium\">Medium</option>\n<option value=\"large\">Large</option>\n</select>\n</div>\n</div>\n</div>\n<div class=\"demo-component-section\">\n<h3>Component Example</h3>\n<div class=\"example-wrapper\">\n<label for=\"enhanced-select-example\" class=\"example-label\">Choose a programming language:</label>\n<enhanced-select\nid=\"enhanced-select-example\"\nplaceholder=\"Select a language\"\ntheme=\"light\"\ndensity=\"comfortable\"\nsize=\"medium\"\nclass=\"rounded\">\n<option value=\"js\">JavaScript</option>\n<option value=\"ts\">TypeScript</option>\n<option value=\"py\">Python</option>\n<option value=\"java\">Java</option>\n<option value=\"cs\">C#</option>\n<option value=\"go\">Go</option>\n<option value=\"rb\">Ruby</option>\n<option value=\"rs\">Rust</option>\n</enhanced-select>\n</div>\n<div class=\"selection-result\">\n<p>Selected value: <span id=\"selected-value\">None</span></p>\n</div>\n</div>\n<div class=\"design-system-tokens\">\n<h3>Design System Tokens</h3>\n<div class=\"token-display\">\n<div class=\"token-group\">\n<h4>Colors</h4>\n<div class=\"token color\" style=\"--color: var(--color-primary);\">--color-primary</div>\n<div class=\"token color\" style=\"--color: var(--color-surface);\">--color-surface</div>\n<div class=\"token color\" style=\"--color: var(--color-border);\">--color-border</div>\n<div class=\"token color\" style=\"--color: var(--color-text);\">--color-text</div>\n</div>\n<div class=\"token-group\">\n<h4>Spacing</h4>\n<div class=\"token spacing\" style=\"--size: var(--spacing-xs);\">--spacing-xs</div>\n<div class=\"token spacing\" style=\"--size: var(--spacing-sm);\">--spacing-sm</div>\n<div class=\"token spacing\" style=\"--size: var(--spacing-md);\">--spacing-md</div>\n<div class=\"token spacing\" style=\"--size: var(--spacing-lg);\">--spacing-lg</div>\n</div>\n<div class=\"token-group\">\n<h4>Typography</h4>\n<div class=\"token text\">--font-family</div>\n<div class=\"token text\">--font-size</div>\n</div>\n</div>\n</div>\n</div>\nCSS\n/* Base styling for the demo interface */\n:root {\n/* Light theme (default) */\n--color-primary: #4a6cf7;\n--color-primary-hover: #3a5cf7;\n--color-surface: #ffffff;\n--color-surface-hover: #f5f8ff;\n--color-border: #d0d7de;\n--color-text: #24292f;\n--color-text-secondary: #57606a;\n--color-background: #f6f8fa;\n/* Spacing tokens */\n--spacing-xs: 4px;\n--spacing-sm: 8px;\n--spacing-md: 16px;\n--spacing-lg: 24px;\n/* Typography */\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n--font-size-sm: 14px;\n--font-size-md: 16px;\n--font-size-lg: 18px;\n/* Interactive states */\n--focus-ring: 0 0 0 2px rgba(74, 108, 247, 0.4);\n/* Transitions */\n--transition-default: all 0.2s ease;\n}\n/* Dark theme variables (will be applied to body) */\n.theme-dark {\n--color-primary: #6c8eff;\n--color-primary-hover: #819fff;\n--color-surface: #1e1e2e;\n--color-surface-hover: #2a2a3a;\n--color-border: #383854;\n--color-text: #e2e4e9;\n--color-text-secondary: #9da3af;\n--color-background: #141420;\n}\n/* Brand theme variables */\n.theme-brand {\n--color-primary: #ff4081;\n--color-primary-hover: #ff5b92;\n--color-surface: #ffffff;\n--color-surface-hover: #fff0f5;\n--color-border: #ffacc5;\n--color-text: #333333;\n--color-text-secondary: #666666;\n--color-background: #fff9fb;\n}\nbody {\nfont-family: var(--font-family);\nbackground-color: var(--color-background);\ncolor: var(--color-text);\ntransition: var(--transition-default);\nmargin: 0;\npadding: 0;\n}\n.container {\nmax-width: 900px;\nmargin: 0 auto;\npadding: 30px 20px;\n}\nh2 {\nmargin-top: 0;\ncolor: var(--color-primary);\nfont-weight: 600;\n}\nh3 {\nmargin-bottom: 15px;\ncolor: var(--color-text);\nfont-weight: 600;\n}\nh4 {\nmargin-bottom: 10px;\ncolor: var(--color-text-secondary);\nfont-weight: 500;\nfont-size: 14px;\ntext-transform: uppercase;\nletter-spacing: 0.5px;\n}\n.intro {\nbackground-color: var(--color-surface);\npadding: var(--spacing-md);\nborder-radius: 8px;\nmargin-bottom: var(--spacing-lg);\nborder-left: 4px solid var(--color-primary);\n}\n.demo-controls {\ndisplay: flex;\nflex-wrap: wrap;\ngap: var(--spacing-md);\nmargin-bottom: var(--spacing-lg);\n}\n.control-group {\nflex: 1;\nmin-width: 280px;\nbackground-color: var(--color-surface);\nborder-radius: 8px;\npadding: var(--spacing-md);\nbox-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n.control {\ndisplay: flex;\nalign-items: center;\nmargin-bottom: var(--spacing-md);\ngap: var(--spacing-md);\n}\n.control label {\nmin-width: 80px;\nfont-weight: 500;\ncolor: var(--color-text-secondary);\n}\n.control.checkbox {\ngap: var(--spacing-sm);\n}\n.control.checkbox label {\nmin-width: auto;\n}\nselect, input[type=\"text\"] {\npadding: 8px 12px;\nborder: 1px solid var(--color-border);\nborder-radius: 4px;\nbackground-color: var(--color-surface);\ncolor: var(--color-text);\nfont-family: inherit;\nfont-size: var(--font-size-sm);\ntransition: var(--transition-default);\n}\nselect:focus, input:focus {\noutline: none;\nbox-shadow: var(--focus-ring);\nborder-color: var(--color-primary);\n}\n.demo-component-section {\nbackground-color: var(--color-surface);\npadding: var(--spacing-lg);\nborder-radius: 8px;\nmargin-bottom: var(--spacing-lg);\nbox-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n.example-wrapper {\nmargin-bottom: var(--spacing-md);\n}\n.example-label {\ndisplay: block;\nmargin-bottom: var(--spacing-sm);\nfont-weight: 500;\ncolor: var(--color-text);\n}\n.selection-result {\nmargin-top: var(--spacing-lg);\npadding: var(--spacing-md);\nbackground-color: var(--color-background);\nborder-radius: 6px;\n}\n#selected-value {\nfont-weight: bold;\ncolor: var(--color-primary);\n}\n.design-system-tokens {\nbackground-color: var(--color-surface);\npadding: var(--spacing-lg);\nborder-radius: 8px;\nbox-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);\n}\n.token-display {\ndisplay: flex;\nflex-wrap: wrap;\ngap: var(--spacing-lg);\n}\n.token-group {\nmin-width: 200px;\n}\n.token {\npadding: var(--spacing-sm);\nmargin-bottom: var(--spacing-sm);\nborder-radius: 4px;\nfont-family: monospace;\nfont-size: 14px;\n}\n.token.color {\nbackground-color: var(--color);\ncolor: white;\ntext-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);\npadding: var(--spacing-md);\ndisplay: flex;\nalign-items: center;\n}\n.token.spacing {\nwidth: 100%;\nheight: var(--size);\nbackground-color: var(--color-primary);\ndisplay: flex;\nalign-items: center;\npadding: 0 var(--spacing-sm);\nwhite-space: nowrap;\noverflow: hidden;\n}\n.token.text {\nbackground-color: var(--color-background);\ncolor: var(--color-text);\n}\nJavaScript\n// Define the EnhancedSelect Web Component\nclass EnhancedSelect extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Component state\nthis._open = false;\nthis._selectedIndex = -1;\nthis._options = [];\nthis._focusedOptionIndex = -1;\n// Bind methods\nthis._handleClickOutside = this._handleClickOutside.bind(this);\nthis._handleKeyDown = this._handleKeyDown.bind(this);\n// Initial render\nthis.render();\n}\n// Specify observed attributes\nstatic get observedAttributes() {\nreturn ['placeholder', 'theme', 'density', 'size', 'disabled', 'required'];\n}\n// Lifecycle: component connected to DOM\nconnectedCallback() {\n// Get child options from light DOM\nthis._processOptions();\n// Add event listener for clicking outside\ndocument.addEventListener('click', this._handleClickOutside);\ndocument.addEventListener('keydown', this._handleKeyDown);\n// Set initial selection if there's a selected option\nconst selectedOption = Array.from(this.querySelectorAll('option')).find(opt => opt.selected);\nif (selectedOption) {\nconst index = this._options.findIndex(opt => opt.value === selectedOption.value);\nif (index !== -1) {\nthis._selectOption(index);\n}\n}\n}\n// Lifecycle: component removed from DOM\ndisconnectedCallback() {\ndocument.removeEventListener('click', this._handleClickOutside);\ndocument.removeEventListener('keydown', this._handleKeyDown);\n}\n// Lifecycle: observed attributes changed\nattributeChangedCallback(name, oldValue, newValue) {\nif (oldValue !== newValue) {\nthis.render();\n}\n}\n// Process child options\n_processOptions() {\nconst options = Array.from(this.querySelectorAll('option'));\nthis._options = options.map(opt => ({\nvalue: opt.value,\nlabel: opt.textContent,\ndisabled: opt.disabled }));\nthis.render();\n}\n// Toggle dropdown\n_toggleDropdown() {\nif (this.disabled) return;\nthis._open = !this._open;\nif (this._open) {\n// Set focus to the selected option or first option\nthis._focusedOptionIndex = this._selectedIndex !== -1 ? this._selectedIndex : 0;\n}\nthis.render();\n}\n// Close dropdown\n_closeDropdown() {\nif (this._open) {\nthis._open = false;\nthis.render();\n}\n}\n// Select an option\n_selectOption(index) {\nif (index < 0 || index >= this._options.length || this._options[index].disabled) {\nreturn;\n}\nthis._selectedIndex = index;\nthis._closeDropdown();\n// Dispatch change event\nthis.dispatchEvent(new CustomEvent('change', {\nbubbles: true,\ncomposed: true,\ndetail: {\nvalue: this._options[index].value,\nlabel: this._options[index].label } }));\n// Update the display of the selected value\nconst selectedValueDisplay = document.getElementById('selected-value');\nif (selectedValueDisplay) {\nselectedValueDisplay.textContent = this._options[index].label;\n}\n}\n// Handle click outside to close dropdown\n_handleClickOutside(event) {\nconst path = event.composedPath();\nif (!path.includes(this) && this._open) {\nthis._closeDropdown();\n}\n}\n// Handle keyboard navigation\n_handleKeyDown(event) {\nif (!this._open) return;\nswitch (event.key) {\ncase 'ArrowDown':\nevent.preventDefault();\nthis._focusedOptionIndex = Math.min(this._focusedOptionIndex + 1, this._options.length - 1);\nthis.render();\nbreak;\ncase 'ArrowUp':\nevent.preventDefault();\nthis._focusedOptionIndex = Math.max(this._focusedOptionIndex - 1, 0);\nthis.render();\nbreak;\ncase 'Enter':\ncase ' ':\nevent.preventDefault();\nif (this._focusedOptionIndex >= 0) {\nthis._selectOption(this._focusedOptionIndex);\n}\nbreak;\ncase 'Escape':\nevent.preventDefault();\nthis._closeDropdown();\nbreak;\ndefault:\nbreak;}\n}\n// Getter/setters for properties\nget value() {\nreturn this._selectedIndex >= 0 ? this._options[this._selectedIndex].value : '';\n}\nset value(val) {\nconst index = this._options.findIndex(opt => opt.value === val);\nif (index !== -1) {\nthis._selectOption(index);\n}\n}\nget disabled() {\nreturn this.hasAttribute('disabled');\n}\nset disabled(value) {\nif (value) {\nthis.setAttribute('disabled', '');\n} else {\nthis.removeAttribute('disabled');\n}\n}\n// Render the component\nrender() {\n// Get attribute values with defaults\nconst theme = this.getAttribute('theme') || 'light';\nconst density = this.getAttribute('density') || 'comfortable';\nconst size = this.getAttribute('size') || 'medium';\nconst disabled = this.hasAttribute('disabled');\nconst required = this.hasAttribute('required');\nconst placeholder = this.getAttribute('placeholder') || 'Select an option';\n// Check if the element has the rounded class\nconst isRounded = this.classList.contains('rounded');\n// Component CSS\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\n--select-padding-y: ${density === 'compact' ? '6px' : '10px'};\n--select-padding-x: ${density === 'compact' ? '10px' : '14px'};\n--select-font-size: ${\nsize === 'small' ? 'var(--font-size-sm)' :\nsize === 'large' ? 'var(--font-size-lg)' :\n'var(--font-size-md)'\n};\n--select-border-radius: ${isRounded ? '20px' : '4px'};\ndisplay: block;\nposition: relative;\nwidth: 100%;\nfont-family: var(--font-family, ${getComputedStyle(document.body).fontFamily});\nfont-size: var(--select-font-size);\ncolor: ${disabled ? 'var(--color-text-secondary, #666)' : 'var(--color-text, #333)'};\n-webkit-tap-highlight-color: transparent;\n}\n.select-container {\nposition: relative;\nwidth: 100%;\n}\n.select-trigger {\ndisplay: flex;\nalign-items: center;\njustify-content: space-between;\nwidth: 100%;\npadding: var(--select-padding-y) var(--select-padding-x);\nbackground-color: var(--color-surface, white);\nborder: 1px solid var(--color-border, #ccc);\nborder-radius: var(--select-border-radius);\ncursor: ${disabled ? 'not-allowed' : 'pointer'};\ntransition: var(--transition-default, all 0.2s ease);\nopacity: ${disabled ? '0.7' : '1'};\nbox-sizing: border-box;\n}\n.select-trigger:hover {\n${!disabled ? 'background-color: var(--color-surface-hover, #f9f9f9);' : ''}\n${!disabled ? 'border-color: var(--color-primary, #4a6cf7);' : ''}\n}\n.select-trigger:focus {\noutline: none;\nbox-shadow: var(--focus-ring, 0 0 0 2px rgba(74, 108, 247, 0.4));\nborder-color: var(--color-primary, #4a6cf7);\n}\n.select-value {\noverflow: hidden;\ntext-overflow: ellipsis;\nwhite-space: nowrap;\ncolor: ${this._selectedIndex === -1 ? 'var(--color-text-secondary, #666)' : 'var(--color-text, #333)'};\n}\n.select-icon {\ndisplay: flex;\nalign-items: center;\nmargin-left: 8px;\ntransition: transform 0.2s ease;\ntransform: ${this._open ? 'rotate(180deg)' : 'rotate(0)'};\n}\n.select-icon svg {\nwidth: 16px;\nheight: 16px;\nfill: currentColor;\n}\n.select-dropdown {\nposition: absolute;\ntop: calc(100% + 4px);\nleft: 0;\nwidth: 100%;\nmax-height: 200px;\noverflow-y: auto;\nbackground-color: var(--color-surface, white);\nborder: 1px solid var(--color-border, #ccc);\nborder-radius: var(--select-border-radius);\nbox-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\nz-index: 100;\ndisplay: ${this._open ? 'block' : 'none'};\n}\n.select-option {\npadding: var(--select-padding-y) var(--select-padding-x);\ncursor: pointer;\ntransition: background-color 0.2s ease;\n}\n.select-option:hover:not(.disabled) {\nbackground-color: var(--color-surface-hover, #f5f5f5);\n}\n.select-option.focused {\nbackground-color: var(--color-surface-hover, #f5f5f5);\n}\n.select-option.selected {\nbackground-color: rgba(var(--color-primary-rgb, 74, 108, 247), 0.1);\ncolor: var(--color-primary, #4a6cf7);\nfont-weight: 500;\n}\n.select-option.disabled {\nopacity: 0.5;\ncursor: not-allowed;\n}\n.required-indicator {\ncolor: ${theme === 'brand' ? '#ff4081' : '#e41c3e'};\nmargin-left: 4px;\n}\n/* Empty state */\n.empty-message {\npadding: var(--select-padding-y) var(--select-padding-x);\ncolor: var(--color-text-secondary, #666);\nfont-style: italic;\n}\n</style>\n<div class=\"select-container\">\n<div\nclass=\"select-trigger\"\ntabindex=\"${disabled ? '-1' : '0'}\"\nrole=\"combobox\"\naria-haspopup=\"listbox\"\naria-expanded=\"${this._open}\"\naria-disabled=\"${disabled}\"\naria-required=\"${required}\"\n@click=\"${() => this._toggleDropdown()}\">\n<div class=\"select-value\">\n${this._selectedIndex !== -1 ? this._options[this._selectedIndex].label : placeholder}\n${required ? '<span class=\"required-indicator\">*</span>' : ''}\n</div>\n<div class=\"select-icon\">\n<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\n<path d=\"M7 10l5 5 5-5z\"/>\n</svg>\n</div>\n</div>\n<div class=\"select-dropdown\" role=\"listbox\">\n${this._options.length === 0 ?\n`<div class=\"empty-message\">No options available</div>` :\nthis._options.map((option, index) => `\n<div\nclass=\"select-option ${index === this._selectedIndex ? 'selected' : ''} ${index === this._focusedOptionIndex ? 'focused' : ''} ${option.disabled ? 'disabled' : ''}\"\nrole=\"option\"\naria-selected=\"${index === this._selectedIndex}\"\n@click=\"${option.disabled ? '' : `this._selectOption(${index})`}\">\n${option.label}\n</div>\n`).join('')\n}\n</div>\n</div>\n`;\n// Fix event listeners that were in the template string\nthis.shadowRoot.querySelectorAll('.select-trigger').forEach(el => {\nel.addEventListener('click', () => this._toggleDropdown());\n});\nthis.shadowRoot.querySelectorAll('.select-option:not(.disabled)').forEach((el, index) => {\nel.addEventListener('click', () => this._selectOption(index));\n});\n}}\n// Register the custom element\ncustomElements.define('enhanced-select', EnhancedSelect);\n// Set up theme controls\ndocument.addEventListener('DOMContentLoaded', () => {\nconst component = document.querySelector('enhanced-select');\nconst selectedValue = document.getElementById('selected-value');\n// Theme selector\ndocument.getElementById('theme-selector').addEventListener('change', e => {\nconst theme = e.target.value;\ncomponent.setAttribute('theme', theme);\n// Apply theme to body for demo purposes\ndocument.body.className = theme === 'default' ? '' : `theme-${theme}`;\n});\n// Density selector\ndocument.getElementById('density-selector').addEventListener('change', e => {\ncomponent.setAttribute('density', e.target.value);\n});\n// Size selector\ndocument.getElementById('size-selector').addEventListener('change', e => {\ncomponent.setAttribute('size', e.target.value);\n});\n// Rounded toggle\ndocument.getElementById('rounded-toggle').addEventListener('change', e => {\nif (e.target.checked) {\ncomponent.classList.add('rounded');\n} else {\ncomponent.classList.remove('rounded');\n}\n// Force re-render to apply the change\ncomponent.render();\n});\n// Disabled toggle\ndocument.getElementById('disabled-toggle').addEventListener('change', e => {\nif (e.target.checked) {\ncomponent.setAttribute('disabled', '');\n} else {\ncomponent.removeAttribute('disabled');\n}\n});\n// Required toggle\ndocument.getElementById('required-toggle').addEventListener('change', e => {\nif (e.target.checked) {\ncomponent.setAttribute('required', '');\n} else {\ncomponent.removeAttribute('required');\n}\n});\n// Listen for change events from the component\ncomponent.addEventListener('change', e => {\nselectedValue.textContent = e.detail.label + ` (${e.detail.value})`;\n});\n// Initialize theme color tokens based on computed style\nfunction updateTokenDisplay() {\ndocument.querySelectorAll('.token.color').forEach(token => {\nconst color = getComputedStyle(token).getPropertyValue('--color');\n// Ensure text contrast\nconst isDark = isColorDark(color);\ntoken.style.color = isDark ? 'white' : 'black';\n});\n}\n// Helper to determine if a color is dark (for contrast)\nfunction isColorDark(color) {\n// Simple approach for demo purposes\nconst rgb = color.match(/\\d+/g);\nif (!rgb) return true;\nconst brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000;\nreturn brightness < 128;\n}\n// Update tokens when theme changes\ndocument.getElementById('theme-selector').addEventListener('change', updateTokenDisplay);\n// Initial update\nupdateTokenDisplay();\n});\nDesign System Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis enhanced select component showcases several key design system features:\nTheming - The component responds to design tokens for colors, spacing, and typography\nConsistent Visual Language - The styling follows design system patterns with proper spacing and visual hierarchy\nAccessibility - The component implements ARIA attributes, keyboard navigation, and focus management\nConfigurability - Properties like density, size, and shape variants provide flexibility within the design system\nComponent API - The interface follows consistent patterns with attributes and properties\nTry changing the theme, density, and other properties to see how the component adapts while maintaining design system consistency. Using CSS custom properties as design tokens is a common way for design-system libraries to let pages restyle a shadow-DOM component.\nContent Widgets\nNews sites and content platforms often use web components for interactive widgets:\nclass ContentRecommendation extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['category', 'count'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (oldValue !== newValue) {\nthis._fetchRecommendations();\n}\n}\nconnectedCallback() {\nthis._fetchRecommendations();\n}\nasync _fetchRecommendations() {\nconst category = this.getAttribute('category') || 'general';\nconst count = parseInt(this.getAttribute('count') || '3', 10);\ntry {\nconst response = await fetch(`/api/recommendations?category=${category}&count=${count}`);\nconst data = await response.json();\nthis._renderRecommendations(data);\n} catch (error) {\nconsole.error('Failed to fetch recommendations:', error);\n}\n}\n_renderRecommendations(items) {\n// Render logic...\n}\n}\ncustomElements.define('content-recommendation', ContentRecommendation);\nThis pattern is used by sites like The Guardian and Washington Post for their recommendation modules.\nHere's a working example of a content recommendation component that demonstrates the key concepts of content widgets in web components:\nHTML\n<div class=\"container\">\n<h2>Content Recommendation Widget Demo</h2>\n<div class=\"controls\">\n<div class=\"control-group\">\n<label for=\"category-select\">Category:</label>\n<select id=\"category-select\">\n<option value=\"technology\">Technology</option>\n<option value=\"business\">Business</option>\n<option value=\"health\">Health & Wellness</option>\n</select>\n<label for=\"count-select\">Articles:</label>\n<select id=\"count-select\">\n<option value=\"2\">2</option>\n<option value=\"3\" selected>3</option>\n<option value=\"4\">4</option>\n</select>\n<button id=\"refresh-btn\">Refresh</button>\n</div>\n</div>\n<div class=\"widget-container\">\n<content-recommendation-widget></content-recommendation-widget>\n</div>\n<div class=\"event-log-section\">\n<h3>Component Events</h3>\n<div id=\"event-log\"></div>\n<button id=\"clear-log\">Clear Log</button>\n</div>\n</div>\nCSS\n:root {\n--color-primary: #3b82f6;\n--color-primary-dark: #2563eb;\n--color-surface: #ffffff;\n--color-text: #1f2937;\n--color-text-secondary: #6b7280;\n--color-border: #e5e7eb;\n--color-background: #f3f4f6;\n--color-skeleton: #e5e7eb;\n--border-radius: 8px;\n--spacing-xs: 4px;\n--spacing-sm: 8px;\n--spacing-md: 16px;\n--spacing-lg: 24px;\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\nbody {\nfont-family: var(--font-family);\nbackground-color: var(--color-background);\ncolor: var(--color-text);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.container {\nmax-width: 800px;\nmargin: 0 auto;\n}\nh2, h3 {\nmargin-top: 0;\ncolor: var(--color-text);\n}\n.controls {\nbackground-color: var(--color-surface);\npadding: var(--spacing-md);\nborder-radius: var(--border-radius);\nmargin-bottom: var(--spacing-lg);\nbox-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n.control-group {\ndisplay: flex;\nflex-wrap: wrap;\ngap: var(--spacing-md);\nalign-items: center;\n}\nlabel {\nfont-weight: 500;\n}\nselect, button {\npadding: 8px 12px;\nborder-radius: 4px;\nfont-family: inherit;\n}\nselect {\nborder: 1px solid var(--color-border);\nbackground-color: var(--color-surface);\n}\nbutton {\nbackground-color: var(--color-primary);\ncolor: white;\nborder: none;\ncursor: pointer;\nfont-weight: 500;\n}\nbutton:hover {\nbackground-color: var(--color-primary-dark);\n}\n#clear-log {\nbackground-color: #6b7280;\nmargin-top: var(--spacing-sm);\n}\n.widget-container {\nborder-radius: var(--border-radius);\noverflow: hidden;\nmargin-bottom: var(--spacing-lg);\nbox-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n.event-log-section {\nbackground-color: var(--color-surface);\npadding: var(--spacing-md);\nborder-radius: var(--border-radius);\nmargin-top: var(--spacing-lg);\n}\n#event-log {\nbackground-color: #f1f5f9;\npadding: var(--spacing-md);\nborder-radius: 4px;\nfont-family: monospace;\nfont-size: 14px;\nheight: 150px;\noverflow-y: auto;\n}\n.log-entry {\nmargin-bottom: var(--spacing-xs);\npadding-bottom: var(--spacing-xs);\nborder-bottom: 1px solid var(--color-border);\n}\n.log-time {\ncolor: var(--color-text-secondary);\nfont-size: 12px;\n}\n/* Animation for loading skeleton */\n@keyframes pulse {\n0% { opacity: 0.6; }\n50% { opacity: 1; }\n100% { opacity: 0.6; }\n}\n.skeleton {\nbackground-color: var(--color-skeleton);\nanimation: pulse 1.5s infinite;\n}\nJavaScript\n// Helper function to log events\nfunction logEvent(message) {\nconst logContainer = document.getElementById('event-log');\nif (!logContainer) return;\nconst entry = document.createElement('div');\nentry.className = 'log-entry';\nconst time = document.createElement('div');\ntime.className = 'log-time';\ntime.textContent = new Date().toLocaleTimeString();\nconst text = document.createElement('div');\ntext.textContent = message;\nentry.appendChild(time);\nentry.appendChild(text);\nlogContainer.appendChild(entry);\nlogContainer.scrollTop = logContainer.scrollHeight;\n}\n// Mock data for recommendations\nconst MOCK_DATA = {\ntechnology: [\n{\nid: 't1',\ntitle: 'The Future of Web Components',\nexcerpt: 'Exploring how web components are transforming front-end development.',\nimage: 'https://picsum.photos/id/1/300/150',\nauthor: 'Alex Chen' },\n{\nid: 't2',\ntitle: 'Building with Custom Elements',\nexcerpt: 'A practical guide to creating reusable UI components.',\nimage: 'https://picsum.photos/id/2/300/150',\nauthor: 'Maria Garcia' },\n{\nid: 't3',\ntitle: 'Shadow DOM Explained',\nexcerpt: 'Understanding encapsulation in modern web development.',\nimage: 'https://picsum.photos/id/3/300/150',\nauthor: 'James Wilson' },\n{\nid: 't4',\ntitle: 'Web Components vs Frameworks',\nexcerpt: 'Comparing native solutions with popular frameworks.',\nimage: 'https://picsum.photos/id/4/300/150',\nauthor: 'Sarah Johnson' }],\nbusiness: [\n{\nid: 'b1',\ntitle: 'Digital Transformation Strategies',\nexcerpt: 'How businesses are adapting to technological change.',\nimage: 'https://picsum.photos/id/5/300/150',\nauthor: 'Michael Roberts' },\n{\nid: 'b2',\ntitle: 'Remote Work in 2025',\nexcerpt: 'The evolving landscape of distributed teams.',\nimage: 'https://picsum.photos/id/6/300/150',\nauthor: 'Jessica Taylor' },\n{\nid: 'b3',\ntitle: 'Sustainable Business Practices',\nexcerpt: 'How environmental responsibility drives success.',\nimage: 'https://picsum.photos/id/7/300/150',\nauthor: 'Robert Anderson' },\n{\nid: 'b4',\ntitle: 'AI in Business Intelligence',\nexcerpt: 'Leveraging artificial intelligence for better decisions.',\nimage: 'https://picsum.photos/id/8/300/150',\nauthor: 'Emily Zhang' }],\nhealth: [\n{\nid: 'h1',\ntitle: 'Mindfulness and Productivity',\nexcerpt: 'How mental wellness practices improve work performance.',\nimage: 'https://picsum.photos/id/9/300/150',\nauthor: 'Dr. Elizabeth Chen' },\n{\nid: 'h2',\ntitle: 'Digital Health Innovations',\nexcerpt: 'New technologies reshaping healthcare delivery.',\nimage: 'https://picsum.photos/id/10/300/150',\nauthor: 'Dr. James Peterson' },\n{\nid: 'h3',\ntitle: 'Nutrition Science Breakthroughs',\nexcerpt: 'Latest research on diet and wellness.',\nimage: 'https://picsum.photos/id/11/300/150',\nauthor: 'Dr. Maria Santos' },\n{\nid: 'h4',\ntitle: 'Sleep Science and Performance',\nexcerpt: 'How quality rest affects overall health.',\nimage: 'https://picsum.photos/id/12/300/150',\nauthor: 'Dr. David Kim' }] };\n// ContentRecommendationWidget component\nclass ContentRecommendationWidget extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Component state\nthis.loading = true;\nthis.error = null;\nthis.articles = [];\n// Initial render\nthis.render();\n}\nconnectedCallback() {\nlogEvent('Component connected to DOM');\nthis.loadArticles();\n// Listen for control changes\ndocument.getElementById('category-select').addEventListener('change', this.handleControlChange.bind(this));\ndocument.getElementById('count-select').addEventListener('change', this.handleControlChange.bind(this));\ndocument.getElementById('refresh-btn').addEventListener('click', this.handleRefresh.bind(this));\ndocument.getElementById('clear-log').addEventListener('click', () => {\ndocument.getElementById('event-log').innerHTML = '';\n});\n}\nhandleControlChange() {\nthis.loadArticles();\n}\nhandleRefresh() {\nlogEvent('Refresh button clicked');\nthis.loadArticles();\n}\n// Load articles based on selected controls\nasync loadArticles() {\nconst category = document.getElementById('category-select').value;\nconst count = parseInt(document.getElementById('count-select').value);\nlogEvent(`Loading articles: category=${category}, count=${count}`);\nthis.loading = true;\nthis.error = null;\nthis.render();\ntry {\n// Simulate API delay\nawait new Promise(resolve => setTimeout(resolve, 1500));\n// Occasionally simulate an error for demo purposes (10% chance)\nif (Math.random() < 0.1) {\nthrow new Error('Failed to fetch recommendations. Please try again.');\n}\n// Get articles for the requested category and limit count\nthis.articles = MOCK_DATA[category]?.slice(0, count) || [];\nthis.loading = false;\nlogEvent(`Loaded ${this.articles.length} articles successfully`);\n// Dispatch event\nthis.dispatchEvent(new CustomEvent('articles-loaded', {\nbubbles: true,\ncomposed: true,\ndetail: { articles: this.articles } }));\n} catch (error) {\nthis.error = error.message;\nthis.loading = false;\nlogEvent(`Error: ${error.message}`);\n// Dispatch error event\nthis.dispatchEvent(new CustomEvent('error', {\nbubbles: true,\ncomposed: true,\ndetail: { error: error.message } }));\n}\nthis.render();\n}\n// Handle article click\nhandleArticleClick(articleId) {\nlogEvent(`Article clicked: ${articleId}`);\n// Dispatch event\nthis.dispatchEvent(new CustomEvent('article-selected', {\nbubbles: true,\ncomposed: true,\ndetail: { articleId } }));\n}\n// Render component\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.container {\nbackground: white;\npadding: 20px;\n}\n.title {\nfont-size: 20px;\nfont-weight: 600;\nmargin: 0 0 16px 0;\n}\n.articles-grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\ngap: 16px;\n}\n.article-card {\nborder: 1px solid #e5e7eb;\nborder-radius: 8px;\noverflow: hidden;\ntransition: transform 0.2s, box-shadow 0.2s;\ncursor: pointer;\n}\n.article-card:hover {\ntransform: translateY(-4px);\nbox-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);\n}\n.article-image {\nwidth: 100%;\nheight: 150px;\nobject-fit: cover;\n}\n.article-content {\npadding: 16px;\n}\n.article-title {\nfont-size: 18px;\nfont-weight: 600;\nmargin: 0 0 8px 0;\n}\n.article-excerpt {\nfont-size: 14px;\ncolor: #6b7280;\nmargin: 0 0 12px 0;\n}\n.article-author {\nfont-size: 12px;\ncolor: #6b7280;\n}\n/* Loading state */\n.loading-container {\npadding: 32px;\ntext-align: center;\n}\n.spinner {\nwidth: 40px;\nheight: 40px;\nborder: 4px solid rgba(0, 0, 0, 0.1);\nborder-left-color: #3b82f6;\nborder-radius: 50%;\nanimation: spin 1s linear infinite;\nmargin: 0 auto;\n}\n.loading-text {\nmargin-top: 16px;\ncolor: #6b7280;\n}\n.skeleton-card {\nborder-radius: 8px;\nheight: 250px;\nbackground: #e5e7eb;\nanimation: pulse 1.5s infinite;\n}\n/* Error state */\n.error-container {\npadding: 32px;\ntext-align: center;\ncolor: #ef4444;\n}\n.retry-button {\nmargin-top: 16px;\npadding: 8px 16px;\nbackground-color: #3b82f6;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\n@keyframes spin {\n0% { transform: rotate(0deg); }\n100% { transform: rotate(360deg); }\n}\n@keyframes pulse {\n0% { opacity: 0.6; }\n50% { opacity: 1; }\n100% { opacity: 0.6; }\n}\n</style>\n<div class=\"container\">\n<h2 class=\"title\">Recommended For You</h2>\n${this.loading ? this.renderLoading() : ''}\n${this.error ? this.renderError() : ''}\n${!this.loading && !this.error ? this.renderArticles() : ''}\n</div>\n`;\n// Add event listeners after rendering\nif (!this.loading && !this.error) {\nthis.shadowRoot.querySelectorAll('.article-card').forEach(card => {\nconst articleId = card.dataset.id;\ncard.addEventListener('click', () => this.handleArticleClick(articleId));\n});\n}\n// Add retry button listener for error state\nif (this.error) {\nconst retryButton = this.shadowRoot.querySelector('.retry-button');\nif (retryButton) {\nretryButton.addEventListener('click', () => this.loadArticles());\n}\n}\n}\n// Render loading state\nrenderLoading() {\nconst count = parseInt(document.getElementById('count-select').value) || 3;\nreturn `\n<div class=\"loading-container\">\n<div class=\"spinner\"></div>\n<p class=\"loading-text\">Loading recommendations...</p>\n<div class=\"articles-grid\">\n${Array(count).fill(0).map(() => `\n<div class=\"skeleton-card\"></div>\n`).join('')}\n</div>\n</div>\n`;\n}\n// Render error state\nrenderError() {\nreturn `\n<div class=\"error-container\">\n<p>${this.error}</p>\n<button class=\"retry-button\">Try Again</button>\n</div>\n`;\n}\n// Render articles grid\nrenderArticles() {\nreturn `\n<div class=\"articles-grid\">\n${this.articles.map(article => `\n<div class=\"article-card\" data-id=\"${article.id}\">\n<img class=\"article-image\" src=\"${article.image}\" alt=\"${article.title}\">\n<div class=\"article-content\">\n<h3 class=\"article-title\">${article.title}</h3>\n<p class=\"article-excerpt\">${article.excerpt}</p>\n<div class=\"article-author\">By ${article.author}</div>\n</div>\n</div>\n`).join('')}\n</div>\n`;\n}}\n// Register the custom element\ncustomElements.define('content-recommendation-widget', ContentRecommendationWidget);\nContent Widget Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis demonstration shows how content-focused components:\nFetch and display dynamic content - Simulating API calls to retrieve articles based on user selections\nHandle loading states - Using skeleton loaders to improve perceived performance\nManage error conditions - Gracefully handling failures with clear user feedback\nCommunicate via events - Dispatching custom events when articles are loaded or selected\nRespond to configuration changes - Updating content based on category and count selections\nThis pattern is commonly used by news sites, blogs, and content platforms to create modular, reusable recommendation widgets that can be placed throughout their applications.\nThis pattern is similar to what you'd find on content-heavy sites like The Guardian or The Washington Post, where web components encapsulate complex content widgets while maintaining clean integration with the surrounding application.\nPerformance Patterns\nLazy-Loading Components\nMany production Web Components implement lazy-loading to improve performance:\n// Only load the component when needed\ndocument.querySelector('#show-comments').addEventListener('click', async () => {\n// Dynamically import the component\nawait import('./components/comment-section.js');\n// Now create and use it\nconst commentSection = document.createElement('comment-section');\ndocument.querySelector('#comments-container').appendChild(commentSection);\n});\nYouTube uses this pattern extensively to load UI components only when needed.\nTo demonstrate the power of lazy loading web components, here's an interactive example that simulates loading resource-intensive UI components only when needed:\nHTML\n<div class=\"container\">\n<h2>Lazy Loading Web Components Demo</h2>\n<div class=\"info-box\">\n<p>This demo shows how lazy loading improves performance by loading heavy components only when needed. Click the buttons below to load different components.</p>\n<p>Each component simulates a heavy resource that would normally slow down initial page load.</p>\n</div>\n<div class=\"performance-meter\">\n<div class=\"meter-label\">Page Weight:</div>\n<div class=\"meter\">\n<div class=\"meter-bar\" id=\"page-weight-bar\"></div>\n</div>\n<div class=\"meter-value\" id=\"page-weight-value\">10KB base</div>\n</div>\n<div class=\"components-dashboard\">\n<div class=\"component-controls\">\n<h3>Available Components</h3>\n<button class=\"load-button\" data-component=\"data-chart\">\n<span class=\"component-name\">Data Chart Component</span>\n<span class=\"component-size\">+250KB</span>\n</button>\n<button class=\"load-button\" data-component=\"media-gallery\">\n<span class=\"component-name\">Media Gallery Component</span>\n<span class=\"component-size\">+320KB</span>\n</button>\n<button class=\"load-button\" data-component=\"user-profile\">\n<span class=\"component-name\">User Profile Component</span>\n<span class=\"component-size\">+180KB</span>\n</button>\n<button id=\"reset-demo\" class=\"reset-button\">\n<span>Reset Demo</span>\n</button>\n</div>\n<div class=\"component-display\">\n<h3>Loaded Components</h3>\n<div id=\"components-container\">\n<div class=\"placeholder-message\">No components loaded yet. Click a button to lazy-load a component.</div>\n</div>\n</div>\n</div>\n<div class=\"event-log\">\n<h3>Loading Events</h3>\n<div id=\"loading-log\"></div>\n<button id=\"clear-log\">Clear Log</button>\n</div>\n</div>\nCSS\n:root {\n--color-primary: #4a6cf7;\n--color-primary-light: #6b84f9;\n--color-gray: #e5e7eb;\n--color-gray-dark: #9ca3af;\n--color-text: #1f2937;\n--color-text-light: #6b7280;\n--color-surface: #ffffff;\n--color-background: #f9fafb;\n--color-success: #10b981;\n--color-warning: #f59e0b;\n--color-info: #3b82f6;\n--border-radius: 8px;\n--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n* {\nbox-sizing: border-box;\n}\nbody {\nfont-family: var(--font-family);\nbackground-color: var(--color-background);\ncolor: var(--color-text);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.container {\nmax-width: 1000px;\nmargin: 0 auto;\n}\nh2 {\nmargin-top: 0;\ncolor: var(--color-text);\n}\nh3 {\nfont-size: 18px;\nfont-weight: 600;\nmargin-top: 0;\nmargin-bottom: 16px;\n}\n.info-box {\nbackground-color: #e0f2fe;\nborder-left: 4px solid var(--color-info);\npadding: 16px;\nmargin-bottom: 24px;\nborder-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n.performance-meter {\nbackground-color: var(--color-surface);\npadding: 16px;\nmargin-bottom: 24px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\n}\n.meter-label {\nfont-weight: 600;\nmargin-bottom: 8px;\n}\n.meter {\nheight: 20px;\nbackground-color: var(--color-gray);\nborder-radius: 10px;\noverflow: hidden;\nmargin-bottom: 8px;\n}\n.meter-bar {\nheight: 100%;\nwidth: 10%;\nbackground-color: var(--color-success);\ntransition: width 0.5s ease;\n}\n.meter-value {\ntext-align: right;\nfont-size: 14px;\ncolor: var(--color-text-light);\n}\n.components-dashboard {\ndisplay: flex;\ngap: 24px;\nmargin-bottom: 24px;\n}\n.component-controls, .component-display {\nflex: 1;\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\n}\n.load-button {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nwidth: 100%;\npadding: 12px 16px;\nmargin-bottom: 12px;\nbackground-color: var(--color-surface);\nborder: 1px solid var(--color-gray);\nborder-radius: var(--border-radius);\ncursor: pointer;\ntransition: all 0.2s;\ntext-align: left;\n}\n.load-button:hover {\nborder-color: var(--color-primary-light);\nbox-shadow: var(--shadow-sm);\n}\n.load-button.loaded {\nbackground-color: #f0f9ff;\nborder-color: var(--color-info);\n}\n.component-name {\nfont-weight: 500;\n}\n.component-size {\ncolor: var(--color-text-light);\nfont-size: 14px;\nfont-weight: normal;\n}\n.placeholder-message {\npadding: 24px 16px;\ntext-align: center;\ncolor: var(--color-text-light);\nborder: 2px dashed var(--color-gray);\nborder-radius: var(--border-radius);\n}\n.loaded-component {\npadding: 16px;\nmargin-bottom: 16px;\nborder: 1px solid var(--color-gray);\nborder-radius: var(--border-radius);\n}\n.event-log {\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\n}\n#loading-log {\nheight: 150px;\npadding: 12px;\nmargin-bottom: 12px;\nbackground-color: #f8fafc;\nborder: 1px solid var(--color-gray);\nborder-radius: var(--border-radius);\noverflow-y: auto;\nfont-family: monospace;\nfont-size: 14px;\n}\n.log-entry {\nmargin-bottom: 8px;\npadding-bottom: 8px;\nborder-bottom: 1px solid var(--color-gray);\n}\n.log-time {\ncolor: var(--color-gray-dark);\n}\nbutton {\npadding: 8px 16px;\nbackground-color: var(--color-primary);\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-weight: 500;\n}\nbutton:hover {\nbackground-color: var(--color-primary-light);\n}\n#clear-log {\nbackground-color: var(--color-gray-dark);\n}\n/* Animation for loading indicator */\n@keyframes spin {\n0% { transform: rotate(0deg); }\n100% { transform: rotate(360deg); }\n}\n.loading-indicator {\ndisplay: inline-block;\nwidth: 20px;\nheight: 20px;\nmargin-right: 8px;\nborder: 3px solid rgba(0, 0, 0, 0.1);\nborder-radius: 50%;\nborder-top-color: var(--color-primary);\nanimation: spin 1s ease-in-out infinite;\n}\n/* Component-specific styles */\n.chart-container {\nheight: 200px;\nbackground-color: #f8fafc;\nborder-radius: var(--border-radius);\noverflow: hidden;\nposition: relative;\n}\n.chart-bar {\nposition: absolute;\nbottom: 0;\nwidth: 30px;\nbackground-color: var(--color-primary);\ntransition: height 0.5s ease;\n}\n.gallery-grid {\ndisplay: grid;\ngrid-template-columns: repeat(3, 1fr);\ngap: 8px;\n}\n.gallery-item {\naspect-ratio: 1 / 1;\nbackground-color: #f1f5f9;\nborder-radius: 4px;\noverflow: hidden;\n}\n.gallery-item img {\nwidth: 100%;\nheight: 100%;\nobject-fit: cover;\n}\n.profile-container {\ndisplay: flex;\nalign-items: center;\ngap: 16px;\n}\n.profile-avatar {\nwidth: 80px;\nheight: 80px;\nborder-radius: 50%;\nbackground-color: #f1f5f9;\noverflow: hidden;\n}\n.profile-avatar img {\nwidth: 100%;\nheight: 100%;\nobject-fit: cover;\n}\n.profile-info {\nflex: 1;\n}\n.profile-name {\nfont-size: 18px;\nfont-weight: 600;\nmargin-bottom: 4px;\n}\n.profile-details {\ncolor: var(--color-text-light);\nfont-size: 14px;\n}\n@media (max-width: 768px) {\n.components-dashboard {\nflex-direction: column;\n}\n}\n.reset-button {\ndisplay: block;\nwidth: 100%;\nmargin-top: 20px;\npadding: 12px 16px;\nbackground-color: #6b7280;\ncolor: white;\nborder: none;\nborder-radius: var(--border-radius);\ncursor: pointer;\nfont-weight: 500;\ntransition: background-color 0.2s;\n}\n.reset-button:hover {\nbackground-color: #4b5563;\n}\n.reset-button:active {\ntransform: translateY(1px);\n}\nJavaScript\n// Helper function to log events\nfunction logEvent(message) {\nconst logContainer = document.getElementById('loading-log');\nif (!logContainer) return;\nconst entry = document.createElement('div');\nentry.className = 'log-entry';\nconst time = document.createElement('span');\ntime.className = 'log-time';\ntime.textContent = `[${new Date().toLocaleTimeString()}] `;\nentry.appendChild(time);\nentry.appendChild(document.createTextNode(message));\nlogContainer.appendChild(entry);\nlogContainer.scrollTop = logContainer.scrollHeight;\n}\n// Update the performance meter\nfunction updatePerformanceMeter(additionalKB) {\nconst totalWeight = 10 + additionalKB; // 10KB base + loaded components\nconst weightPercentage = Math.min(100, totalWeight / 800 * 100); // Cap at 100%\nconst weightBar = document.getElementById('page-weight-bar');\nconst weightValue = document.getElementById('page-weight-value');\nweightBar.style.width = `${weightPercentage}%`;\nweightValue.textContent = `${totalWeight}KB loaded`;\n// Change color based on weight\nif (weightPercentage > 70) {\nweightBar.style.backgroundColor = '#ef4444'; // Red for heavy\n} else if (weightPercentage > 40) {\nweightBar.style.backgroundColor = '#f59e0b'; // Amber for medium\n} else {\nweightBar.style.backgroundColor = '#10b981'; // Green for light\n}\n}\n// Simulate loading delay\nfunction simulateLoading(ms) {\nreturn new Promise(resolve => setTimeout(resolve, ms));\n}\n// Component definitions - In a real app, these would be in separate files\nclass DataChartComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n}\nconnectedCallback() {\nlogEvent('DataChartComponent: Initializing...');\n// Simulate resource-intensive initialization\nsetTimeout(() => {\nthis.render();\nlogEvent('DataChartComponent: Rendered and ready');\n}, 500);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.chart-container {\nheight: 200px;\nbackground-color: #f8fafc;\nborder-radius: 8px;\npadding: 16px;\nposition: relative;\ndisplay: flex;\nalign-items: flex-end;\njustify-content: space-around;\n}\n.chart-bar {\nwidth: 40px;\nbackground-color: #4a6cf7;\ntransition: height 0.5s ease;\n}\n.title {\nfont-weight: 600;\nmargin-bottom: 12px;\n}\n</style>\n<div>\n<div class=\"title\">Data Visualization Chart</div>\n<div class=\"chart-container\">\n<div class=\"chart-bar\" style=\"height: 60%;\"></div>\n<div class=\"chart-bar\" style=\"height: 80%;\"></div>\n<div class=\"chart-bar\" style=\"height: 40%;\"></div>\n<div class=\"chart-bar\" style=\"height: 90%;\"></div>\n<div class=\"chart-bar\" style=\"height: 50%;\"></div>\n<div class=\"chart-bar\" style=\"height: 70%;\"></div>\n</div>\n</div>\n`;\n// Animate bars for visual effect\nconst bars = this.shadowRoot.querySelectorAll('.chart-bar');\nbars.forEach((bar, index) => {\nconst originalHeight = bar.style.height;\nbar.style.height = '0%';\nsetTimeout(() => {\nbar.style.height = originalHeight;\n}, 100 * index);\n});\n}}\nclass MediaGalleryComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n}\nconnectedCallback() {\nlogEvent('MediaGalleryComponent: Initializing...');\n// Simulate resource-intensive initialization\nsetTimeout(() => {\nthis.render();\nlogEvent('MediaGalleryComponent: Rendered and ready');\n}, 700);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.title {\nfont-weight: 600;\nmargin-bottom: 12px;\n}\n.gallery-grid {\ndisplay: grid;\ngrid-template-columns: repeat(3, 1fr);\ngap: 8px;\n}\n.gallery-item {\naspect-ratio: 1 / 1;\nbackground-color: #f1f5f9;\nborder-radius: 4px;\noverflow: hidden;\nopacity: 0;\ntransform: scale(0.9);\ntransition: opacity 0.3s ease, transform 0.3s ease;\n}\n.gallery-item img {\nwidth: 100%;\nheight: 100%;\nobject-fit: cover;\n}\n</style>\n<div>\n<div class=\"title\">Media Gallery</div>\n<div class=\"gallery-grid\">\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/1/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/2/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/3/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/4/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/5/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/6/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/7/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/8/200\"></div>\n<div class=\"gallery-item\"><img src=\"https://picsum.photos/id/9/200\"></div>\n</div>\n</div>\n`;\n// Animate items appearing for visual effect\nconst items = this.shadowRoot.querySelectorAll('.gallery-item');\nitems.forEach((item, index) => {\nsetTimeout(() => {\nitem.style.opacity = '1';\nitem.style.transform = 'scale(1)';\n}, 100 * index);\n});\n}}\nclass UserProfileComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n}\nconnectedCallback() {\nlogEvent('UserProfileComponent: Initializing...');\n// Simulate resource-intensive initialization\nsetTimeout(() => {\nthis.render();\nlogEvent('UserProfileComponent: Rendered and ready');\n}, 600);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.profile-container {\ndisplay: flex;\nalign-items: center;\ngap: 16px;\n}\n.profile-avatar {\nwidth: 80px;\nheight: 80px;\nborder-radius: 50%;\nbackground-color: #f1f5f9;\noverflow: hidden;\nopacity: 0;\ntransform: translateY(10px);\ntransition: opacity 0.3s ease, transform 0.3s ease;\n}\n.profile-avatar img {\nwidth: 100%;\nheight: 100%;\nobject-fit: cover;\n}\n.profile-info {\nflex: 1;\nopacity: 0;\ntransform: translateY(10px);\ntransition: opacity 0.3s ease, transform 0.3s ease;\ntransition-delay: 0.2s;\n}\n.profile-name {\nfont-size: 18px;\nfont-weight: 600;\nmargin-bottom: 4px;\n}\n.profile-details {\ncolor: #6b7280;\nfont-size: 14px;\nmargin-bottom: 8px;\n}\n.profile-bio {\nfont-size: 14px;\nline-height: 1.5;\n}\n</style>\n<div class=\"profile-container\">\n<div class=\"profile-avatar\">\n<img src=\"https://randomuser.me/api/portraits/women/42.jpg\">\n</div>\n<div class=\"profile-info\">\n<div class=\"profile-name\">Sarah Johnson</div>\n<div class=\"profile-details\">Front-end Developer • San Francisco</div>\n<div class=\"profile-bio\">\nWeb component specialist with 5+ years of experience building scalable,\naccessible UI components for enterprise applications.\n</div>\n</div>\n</div>\n`;\n// Animate profile appearing for visual effect\nsetTimeout(() => {\nthis.shadowRoot.querySelector('.profile-avatar').style.opacity = '1';\nthis.shadowRoot.querySelector('.profile-avatar').style.transform = 'translateY(0)';\nthis.shadowRoot.querySelector('.profile-info').style.opacity = '1';\nthis.shadowRoot.querySelector('.profile-info').style.transform = 'translateY(0)';\n}, 100);\n}}\n// Track loaded components\nconst loadedComponents = {\n'data-chart': false,\n'media-gallery': false,\n'user-profile': false };\n// Handle component loading\nasync function loadComponent(componentName) {\n// Check if already loaded\nif (loadedComponents[componentName]) {\nlogEvent(`${componentName} is already loaded`);\nreturn;\n}\nconst componentsContainer = document.getElementById('components-container');\nconst componentSizes = {\n'data-chart': 250,\n'media-gallery': 320,\n'user-profile': 180 };\n// Remove placeholder if present\nconst placeholder = componentsContainer.querySelector('.placeholder-message');\nif (placeholder) {\nplaceholder.remove();\n}\n// Create component container\nconst componentContainer = document.createElement('div');\ncomponentContainer.className = 'loaded-component';\ncomponentContainer.innerHTML = `\n<div class=\"loading-indicator\"></div>\n<span>Loading ${componentName}...</span>\n`;\ncomponentsContainer.prepend(componentContainer);\n// Mark button as loading\nconst button = document.querySelector(`[data-component=\"${componentName}\"]`);\nbutton.disabled = true;\nbutton.innerHTML = `\n<span class=\"component-name\">Loading ${componentName}...</span>\n<span class=\"component-size\">+${componentSizes[componentName]}KB</span>\n`;\n// Log loading start\nlogEvent(`Started loading ${componentName}`);\ntry {\n// In a real app, this would be a dynamic import\n// await import(`./components/${componentName}.js`);\n// Simulate loading delay based on component size\nconst loadTime = componentSizes[componentName] / 50; // ~5-6 seconds for largest component\nawait simulateLoading(loadTime);\n// Register custom element if not already registered\nif (!customElements.get(componentName)) {\nswitch (componentName) {\ncase 'data-chart':\ncustomElements.define('data-chart', DataChartComponent);\nbreak;\ncase 'media-gallery':\ncustomElements.define('media-gallery', MediaGalleryComponent);\nbreak;\ncase 'user-profile':\ncustomElements.define('user-profile', UserProfileComponent);\nbreak;}\n}\n// Update component container\ncomponentContainer.innerHTML = `<${componentName}></${componentName}>`;\n// Mark as loaded\nloadedComponents[componentName] = true;\nbutton.disabled = false;\nbutton.classList.add('loaded');\nbutton.innerHTML = `\n<span class=\"component-name\">${componentName} (Loaded)</span>\n<span class=\"component-size\">+${componentSizes[componentName]}KB</span>\n`;\n// Update performance meter\nlet totalLoaded = 0;\nfor (const component in loadedComponents) {\nif (loadedComponents[component]) {\ntotalLoaded += componentSizes[component];\n}\n}\nupdatePerformanceMeter(totalLoaded);\n// Log loading completion\nlogEvent(`Successfully loaded ${componentName} (+${componentSizes[componentName]}KB)`);\n} catch (error) {\ncomponentContainer.innerHTML = `\n<div style=\"color: #ef4444;\">Error loading ${componentName}: ${error.message}</div>\n`;\nbutton.disabled = false;\nlogEvent(`Error loading ${componentName}: ${error.message}`);\n}\n}\n// Initialize the demo\ndocument.addEventListener('DOMContentLoaded', () => {\n// Set up click handlers for load buttons\ndocument.querySelectorAll('.load-button').forEach(button => {\nconst componentName = button.dataset.component;\nbutton.addEventListener('click', () => loadComponent(componentName));\n});\n// Clear log button\ndocument.getElementById('clear-log').addEventListener('click', () => {\ndocument.getElementById('loading-log').innerHTML = '';\n});\n// Log startup\nlogEvent('Demo initialized - 10KB base page loaded');\nlogEvent('Components will be lazy-loaded on demand');\n});\n// Reset demo function\nfunction resetDemo() {\n// Log reset event\nlogEvent('Resetting demo...');\n// Reset loaded components tracking\nfor (const component in loadedComponents) {\nloadedComponents[component] = false;\n}\n// Reset performance meter\nupdatePerformanceMeter(0);\n// Reset component buttons\ndocument.querySelectorAll('.load-button').forEach(button => {\nbutton.classList.remove('loaded');\nbutton.disabled = false;\nconst componentName = button.dataset.component;\nconst componentSize = {\n'data-chart': 250,\n'media-gallery': 320,\n'user-profile': 180 }[\ncomponentName];\nbutton.innerHTML = `\n<span class=\"component-name\">${componentName}</span>\n<span class=\"component-size\">+${componentSize}KB</span>\n`;\n});\n// Clear components container and add placeholder\nconst componentsContainer = document.getElementById('components-container');\ncomponentsContainer.innerHTML = `\n<div class=\"placeholder-message\">No components loaded yet. Click a button to lazy-load a component.</div>\n`;\n// Log completion\nlogEvent('Demo reset complete - Back to 10KB base page');\n}\n// Initialize the demo\ndocument.addEventListener('DOMContentLoaded', () => {\n// Set up click handlers for load buttons\ndocument.querySelectorAll('.load-button').forEach(button => {\nconst componentName = button.dataset.component;\nbutton.addEventListener('click', () => loadComponent(componentName));\n});\n// Reset button handler\ndocument.getElementById('reset-demo').addEventListener('click', resetDemo);\n// Clear log button\ndocument.getElementById('clear-log').addEventListener('click', () => {\ndocument.getElementById('loading-log').innerHTML = '';\n});\n// Log startup\nlogEvent('Demo initialized - 10KB base page loaded');\nlogEvent('Components will be lazy-loaded on demand');\n});\nLazy-Loading Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis demo shows how lazy loading can dramatically improve initial page load performance by:\nReducing initial page weight - Only the core 10KB of code loads initially\nLoading components on demand - Each component is loaded only when requested\nProviding visual feedback - The performance meter shows the impact of each loaded component\nUsing dynamic imports - Components are loaded asynchronously without blocking the main thread\nTry clicking the buttons to load different components and watch how the page weight increases only when new components are added. This pattern is used extensively by YouTube for their video player interface, where heavy components are loaded only when the user interacts with them.\nConstructable Stylesheets\nFor components used multiple times on a page, shared stylesheets improve performance:\n// Create once, use many times\nconst sheet = new CSSStyleSheet();\nsheet.replaceSync(`\n:host {\ndisplay: block;\npadding: 16px;\n}\n.content {\ncolor: var(--text-color, #333);\n}\n`);\nclass ReusableCard extends HTMLElement {\nconstructor() {\nsuper();\nconst shadow = this.attachShadow({ mode: 'open' });\n// Use the shared stylesheet\nshadow.adoptedStyleSheets = [sheet];\nshadow.innerHTML = `<div class=\"content\"><slot></slot></div>`;\n}\n}\nTo better understand the performance benefits of Constructable Stylesheets, this interactive demo demonstrates the difference between traditional inline styles (duplicated for each component) and shared stylesheets (reused across all components):\nHTML\n<div class=\"container\">\n<h2>Constructable Stylesheets Performance Demo</h2>\n<div class=\"info-box\">\n<p><strong>Constructable Stylesheets</strong> allow Web Components to share a single stylesheet across multiple instances, improving performance and memory usage.</p>\n<p>This demo compares traditional inline styles (each component has its own copy) vs. Constructable Stylesheets (all components share one stylesheet).</p>\n</div>\n<div class=\"controls\">\n<div class=\"method-selector\">\n<h3>Select Styling Method:</h3>\n<div class=\"toggle-container\">\n<label class=\"toggle\">\n<input type=\"radio\" name=\"style-method\" value=\"traditional\" checked>\n<span class=\"toggle-label\">Traditional</span>\n<span class=\"description\">Each component has its own copy of styles</span>\n</label>\n<label class=\"toggle\">\n<input type=\"radio\" name=\"style-method\" value=\"constructable\">\n<span class=\"toggle-label\">Constructable Stylesheets</span>\n<span class=\"description\">All components share one stylesheet</span>\n</label>\n</div>\n</div>\n<div class=\"component-controls\">\n<h3>Add Components:</h3>\n<div class=\"button-group\">\n<button id=\"add-10\" class=\"btn\">Add 10</button>\n<button id=\"add-50\" class=\"btn\">Add 50</button>\n<button id=\"add-100\" class=\"btn\">Add 100</button>\n<button id=\"clear\" class=\"btn btn-secondary\">Clear All</button>\n</div>\n</div>\n</div>\n<div class=\"metrics\">\n<div class=\"metric-box\">\n<h4>Component Count</h4>\n<div class=\"metric-value\" id=\"component-count\">0</div>\n</div>\n<div class=\"metric-box\">\n<h4>Memory Usage</h4>\n<div class=\"metric-value\" id=\"memory-usage\">-</div>\n</div>\n<div class=\"metric-box\">\n<h4>Creation Time</h4>\n<div class=\"metric-value\" id=\"creation-time\">-</div>\n</div>\n</div>\n<div class=\"component-display\">\n<h3>Component Container <span id=\"display-method\" class=\"method-tag traditional\">Traditional Method</span></h3>\n<div id=\"components-container\" class=\"components-grid\"></div>\n</div>\n<div class=\"explanation\">\n<h3>How It Works</h3>\n<div class=\"code-comparison\">\n<div class=\"code-block\">\n<h4>Traditional Approach</h4>\n<pre><code>// Each component has its own copy of styles\nfunction createComponent() {\nconst shadow = this.attachShadow({mode: 'open'});\n// Styles duplicated for EACH component\nshadow.innerHTML = `\n&lt;style&gt;\n.card {\nbackground: white;\nbox-shadow: 0 2px 8px rgba(0,0,0,0.1);\n/* More styles... */\n}\n&lt;/style&gt;\n&lt;div class=\"card\"&gt;Component content&lt;/div&gt;\n`;\n}</code></pre>\n</div>\n<div class=\"code-block\">\n<h4>Constructable Stylesheets</h4>\n<pre><code>// Create stylesheet ONCE, share across all components\nconst sheet = new CSSStyleSheet();\nsheet.replaceSync(`\n.card {\nbackground: white;\nbox-shadow: 0 2px 8px rgba(0,0,0,0.1);\n/* More styles... */\n}\n`);\nfunction createComponent() {\nconst shadow = this.attachShadow({mode: 'open'});\n// Reuse the same stylesheet\nshadow.adoptedStyleSheets = [sheet];\nshadow.innerHTML = `&lt;div class=\"card\"&gt;Component content&lt;/div&gt;`;\n}</code></pre>\n</div>\n</div>\n</div>\n</div>\nCSS\n:root {\n--color-primary: #4a6cf7;\n--color-primary-light: #e6ecfe;\n--color-traditional: #f97316;\n--color-constructable: #10b981;\n--color-gray: #e5e7eb;\n--color-gray-dark: #9ca3af;\n--color-text: #1f2937;\n--color-text-light: #6b7280;\n--color-surface: #ffffff;\n--color-background: #f9fafb;\n--border-radius: 8px;\n--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n* {\nbox-sizing: border-box;\n}\nbody {\nfont-family: var(--font-family);\nbackground-color: var(--color-background);\ncolor: var(--color-text);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.container {\nmax-width: 1000px;\nmargin: 0 auto;\n}\nh2 {\nfont-size: 24px;\nmargin-top: 0;\nmargin-bottom: 24px;\n}\nh3 {\nfont-size: 18px;\nmargin-top: 0;\nmargin-bottom: 12px;\n}\nh4 {\nfont-size: 16px;\nmargin-top: 0;\nmargin-bottom: 8px;\n}\n.info-box {\nbackground-color: var(--color-primary-light);\nborder-left: 4px solid var(--color-primary);\npadding: 16px;\nmargin-bottom: 24px;\nborder-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n.controls {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\nmargin-bottom: 24px;\n}\n.method-selector, .component-controls {\nflex: 1;\nmin-width: 300px;\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\n}\n.toggle-container {\ndisplay: flex;\nflex-direction: column;\ngap: 12px;\n}\n.toggle {\ndisplay: flex;\nflex-direction: column;\nposition: relative;\npadding: 12px;\nborder: 1px solid var(--color-gray);\nborder-radius: var(--border-radius);\ncursor: pointer;\ntransition: all 0.2s;\n}\n.toggle:hover {\nborder-color: var(--color-primary);\n}\n.toggle input[type=\"radio\"] {\nposition: absolute;\nopacity: 0;\nwidth: 0;\nheight: 0;\n}\n.toggle input[type=\"radio\"]:checked + .toggle-label {\ncolor: var(--color-primary);\nfont-weight: 600;\n}\n.toggle input[type=\"radio\"]:checked + .toggle-label::before {\ncontent: '✓ ';\n}\n.toggle-label {\nfont-weight: 500;\nmargin-bottom: 4px;\n}\n.description {\nfont-size: 14px;\ncolor: var(--color-text-light);\n}\n.toggle:has(input[value=\"traditional\"]:checked) {\nbackground-color: rgba(249, 115, 22, 0.1);\nborder-color: var(--color-traditional);\n}\n.toggle:has(input[value=\"constructable\"]:checked) {\nbackground-color: rgba(16, 185, 129, 0.1);\nborder-color: var(--color-constructable);\n}\n.button-group {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\n}\n.btn {\npadding: 8px 16px;\nbackground-color: var(--color-primary);\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\nfont-weight: 500;\ntransition: background-color 0.2s;\n}\n.btn:hover {\nbackground-color: #3a5cf7;\n}\n.btn-secondary {\nbackground-color: var(--color-gray-dark);\n}\n.btn-secondary:hover {\nbackground-color: #8b97a7;\n}\n.metrics {\ndisplay: flex;\ngap: 16px;\nmargin-bottom: 24px;\nflex-wrap: wrap;\n}\n.metric-box {\nflex: 1;\nmin-width: 200px;\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\ntext-align: center;\n}\n.metric-box h4 {\nmargin: 0 0 8px 0;\nfont-weight: 500;\ncolor: var(--color-text-light);\n}\n.metric-value {\nfont-size: 24px;\nfont-weight: 600;\ncolor: var(--color-primary);\n}\n.component-display {\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\nmargin-bottom: 24px;\n}\n.method-tag {\nfont-size: 14px;\npadding: 4px 8px;\nborder-radius: 4px;\nfont-weight: normal;\nvertical-align: middle;\n}\n.method-tag.traditional {\nbackground-color: rgba(249, 115, 22, 0.1);\ncolor: var(--color-traditional);\n}\n.method-tag.constructable {\nbackground-color: rgba(16, 185, 129, 0.1);\ncolor: var(--color-constructable);\n}\n.components-grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\ngap: 16px;\nmin-height: 200px;\nmargin-top: 16px;\npadding: 16px;\nborder: 2px dashed var(--color-gray);\nborder-radius: var(--border-radius);\n}\n.empty-message {\ngrid-column: 1 / -1;\ntext-align: center;\ncolor: var(--color-text-light);\npadding: 40px 0;\n}\n.explanation {\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\n}\n.code-comparison {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 16px;\nmargin-top: 16px;\n}\n.code-block {\nflex: 1;\nmin-width: 300px;\nbackground-color: var(--color-background);\nborder-radius: var(--border-radius);\noverflow: hidden;\n}\n.code-block h4 {\nmargin: 0;\npadding: 12px 16px;\nbackground-color: #f1f5f9;\nborder-bottom: 1px solid var(--color-gray);\n}\n.code-block pre {\nmargin: 0;\npadding: 16px;\nfont-family: monospace;\nfont-size: 14px;\noverflow-x: auto;\nbackground-color: #f8fafc;\n}\n/* Styling for the actual demo components */\n.demo-component {\nborder-radius: var(--border-radius);\noverflow: hidden;\ntransition: transform 0.2s;\n}\n.demo-component:hover {\ntransform: translateY(-4px);\n}\n.demo-component.traditional {\nbox-shadow: 0 2px 8px rgba(249, 115, 22, 0.2);\n}\n.demo-component.constructable {\nbox-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);\n}\n@media (max-width: 768px) {\n.controls {\nflex-direction: column;\n}\n.code-comparison {\nflex-direction: column;\n}\n}\nJavaScript\n// Initialize shared stylesheet for the Constructable Stylesheets approach\nconst sharedStyleSheet = new CSSStyleSheet();\nsharedStyleSheet.replaceSync(`\n.card {\nbackground-color: white;\nborder-radius: 8px;\npadding: 16px;\nheight: 120px;\ndisplay: flex;\nflex-direction: column;\njustify-content: space-between;\ntransition: transform 0.2s, box-shadow 0.2s;\n}\n.card:hover {\ntransform: translateY(-4px);\n}\n.card-header {\nfont-weight: 600;\nmargin-bottom: 8px;\n}\n.card-content {\nfont-size: 14px;\ncolor: #6b7280;\nflex-grow: 1;\n}\n.card-footer {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nfont-size: 12px;\ncolor: #9ca3af;\nmargin-top: 8px;\n}\n`);\n// Track component count and styling method\nlet componentCount = 0;\nlet currentMethod = 'traditional';\n// Define the traditional component (each instance has its own styles)\nclass TraditionalComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst shadow = this.attachShadow({ mode: 'open' });\n// Each instance gets its own copy of the styles\nshadow.innerHTML = `\n<style>\n.card {\nbackground-color: white;\nborder-radius: 8px;\npadding: 16px;\nheight: 120px;\ndisplay: flex;\nflex-direction: column;\njustify-content: space-between;\ntransition: transform 0.2s, box-shadow 0.2s;\n}\n.card:hover {\ntransform: translateY(-4px);\n}\n.card-header {\nfont-weight: 600;\nmargin-bottom: 8px;\n}\n.card-content {\nfont-size: 14px;\ncolor: #6b7280;\nflex-grow: 1;\n}\n.card-footer {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nfont-size: 12px;\ncolor: #9ca3af;\nmargin-top: 8px;\n}\n</style>\n<div class=\"card\">\n<div class=\"card-header\">Component #${++componentCount}</div>\n<div class=\"card-content\">This component has its own copy of styles (${this.getBytesCount()} bytes)</div>\n<div class=\"card-footer\">\n<span>Traditional Method</span>\n<span>ID: ${Math.floor(Math.random() * 1000)}</span>\n</div>\n</div>\n`;\n}\ngetBytesCount() {\n// Estimate the size of styles in bytes\nreturn 450; // Approximation of the CSS size\n}}\n// Define the optimized component (shares stylesheet)\nclass OptimizedComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst shadow = this.attachShadow({ mode: 'open' });\n// Use the shared stylesheet\nshadow.adoptedStyleSheets = [sharedStyleSheet];\n// Only HTML content, no styles\nshadow.innerHTML = `\n<div class=\"card\">\n<div class=\"card-header\">Component #${++componentCount}</div>\n<div class=\"card-content\">This component shares styles (0 bytes added)</div>\n<div class=\"card-footer\">\n<span>Constructable Stylesheet</span>\n<span>ID: ${Math.floor(Math.random() * 1000)}</span>\n</div>\n</div>\n`;\n}\ngetBytesCount() {\n// No additional style bytes since it uses the shared stylesheet\nreturn 0;\n}}\n// Register custom elements\ncustomElements.define('traditional-component', TraditionalComponent);\ncustomElements.define('optimized-component', OptimizedComponent);\n// Helper functions\nfunction updateMetrics() {\ndocument.getElementById('component-count').textContent = componentCount;\n// Calculate estimated memory usage\nlet memoryUsage = 10; // Base page KB\nif (currentMethod === 'traditional') {\nmemoryUsage += componentCount * 0.45; // Each component adds ~450 bytes of CSS\n} else {\nmemoryUsage += 0.45; // Just one copy of styles shared by all components\n}\ndocument.getElementById('memory-usage').textContent = `~${memoryUsage.toFixed(1)} KB`;\n}\nfunction clearComponents() {\nconst container = document.getElementById('components-container');\ncontainer.innerHTML = '<div class=\"empty-message\">No components added yet. Click a button above to add components.</div>';\ncomponentCount = 0;\nupdateMetrics();\ndocument.getElementById('creation-time').textContent = '-';\n}\nfunction addComponents(count) {\nconst container = document.getElementById('components-container');\n// Remove empty message if present\nconst emptyMessage = container.querySelector('.empty-message');\nif (emptyMessage) {\nemptyMessage.remove();\n}\n// Measure creation time\nconst startTime = performance.now();\n// Create elements\nfor (let i = 0; i < count; i++) {\nconst component = currentMethod === 'traditional' ?\ndocument.createElement('traditional-component') :\ndocument.createElement('optimized-component');\ncomponent.className = `demo-component ${currentMethod}`;\ncontainer.appendChild(component);\n}\n// Calculate creation time\nconst endTime = performance.now();\nconst creationTime = endTime - startTime;\ndocument.getElementById('creation-time').textContent = `${creationTime.toFixed(2)} ms`;\n// Update metrics\nupdateMetrics();\n}\nfunction switchMethod(method) {\ncurrentMethod = method;\ndocument.getElementById('display-method').textContent =\nmethod === 'traditional' ? 'Traditional Method' : 'Constructable Stylesheets';\ndocument.getElementById('display-method').className =\n`method-tag ${method}`;\n// Clear existing components\nclearComponents();\n}\n// Set up event listeners\ndocument.addEventListener('DOMContentLoaded', () => {\n// Method selection\ndocument.querySelectorAll('input[name=\"style-method\"]').forEach(radio => {\nradio.addEventListener('change', e => {\nswitchMethod(e.target.value);\n});\n});\n// Add component buttons\ndocument.getElementById('add-10').addEventListener('click', () => addComponents(10));\ndocument.getElementById('add-50').addEventListener('click', () => addComponents(50));\ndocument.getElementById('add-100').addEventListener('click', () => addComponents(100));\n// Clear button\ndocument.getElementById('clear').addEventListener('click', clearComponents);\n// Initialize with empty message\nclearComponents();\n});\nConstructable Stylesheet Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nTry adding multiple components with both methods to see how Constructable Stylesheets significantly reduce memory usage and improve performance as the number of components increases. This technique is especially valuable in applications like YouTube, where dozens or hundreds of component instances might appear on a single page.\nAccessibility in Production Web Components\nSuccessful Web Components in production prioritize accessibility:\nclass AccessibleTabPanel extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.shadowRoot.innerHTML = `\n<div class=\"tabs\" role=\"tablist\">\n<slot name=\"tab\"></slot>\n</div>\n<div class=\"panels\">\n<slot name=\"panel\"></slot>\n</div>\n`;\n// Add keyboard navigation, ARIA attributes, etc.\n}\nconnectedCallback() {\n// Set up accessibility requirements\nconst tabs = this.querySelectorAll('[slot=\"tab\"]');\nconst panels = this.querySelectorAll('[slot=\"panel\"]');\ntabs.forEach((tab, index) => {\n// Set ARIA attributes\ntab.setAttribute('role', 'tab');\ntab.setAttribute('aria-selected', index === 0 ? 'true' : 'false');\ntab.setAttribute('tabindex', index === 0 ? '0' : '-1');\ntab.setAttribute('aria-controls', `panel-${index}`);\n// Set panel attributes\npanels[index].setAttribute('role', 'tabpanel');\npanels[index].setAttribute('id', `panel-${index}`);\npanels[index].setAttribute('aria-labelledby', tab.id);\npanels[index].hidden = index !== 0;\n// Add event listeners for keyboard navigation\n// ...\n});\n}\n}\nPublished design systems such as Spectrum Web Components document accessibility work like this; read their docs for the details.\nAccessibility is a core part of web components. Let's explore an accessible tab panel component that demonstrates proper keyboard navigation and ARIA attributes:\nHTML\n<div class=\"container\">\n<h2>Accessible Tab Panel Component Demo</h2>\n<div class=\"intro-box\">\n<h3>Accessibility Features</h3>\n<ul>\n<li>Proper ARIA roles, states, and properties</li>\n<li>Keyboard navigation between tabs</li>\n<li>Focus management and visible focus states</li>\n<li>Screen reader announcements</li>\n</ul>\n</div>\n<div class=\"keyboard-instructions\">\n<h3>Keyboard Navigation</h3>\n<ul>\n<li><kbd>Tab</kbd> to focus on the tab list</li>\n<li><kbd>→</kbd> / <kbd>←</kbd> to navigate between tabs</li>\n<li><kbd>Home</kbd> to move to the first tab</li>\n<li><kbd>End</kbd> to move to the last tab</li>\n</ul>\n</div>\n<div class=\"component-container\">\n<accessible-tab-panel id=\"demo-tabs\">\n<div slot=\"tab\">HTML</div>\n<div slot=\"panel\">\n<h3>HTML</h3>\n<p>HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.</p>\n<p>It defines the meaning and structure of web content. HTML elements are the building blocks of HTML pages. With HTML constructs, images and other objects such as interactive forms may be embedded into the rendered page.</p>\n</div>\n<div slot=\"tab\">CSS</div>\n<div slot=\"panel\">\n<h3>CSS</h3>\n<p>CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in HTML or XML.</p>\n<p>CSS is designed to enable the separation of presentation and content, including layout, colors, and fonts. This separation improves content accessibility, provides more flexibility and control over presentation characteristics.</p>\n</div>\n<div slot=\"tab\">JavaScript</div>\n<div slot=\"panel\">\n<h3>JavaScript</h3>\n<p>JavaScript is a programming language that enables interactive web pages and is an essential part of web applications.</p>\n<p>It is used for client-side scripting to make web pages dynamic and interactive. JavaScript is also used in server-side programming, game development, and the creation of desktop and mobile applications.</p>\n</div>\n<div slot=\"tab\">Web Components</div>\n<div slot=\"panel\">\n<h3>Web Components</h3>\n<p>Web Components are a set of standards that allow developers to create custom, reusable HTML elements with encapsulated functionality.</p>\n<p>They consist of Custom Elements, Shadow DOM, and HTML Templates. Web Components enable component-based development in the browser without requiring external libraries or frameworks.</p>\n</div>\n</accessible-tab-panel>\n</div>\n<div class=\"live-region\" aria-live=\"polite\" id=\"a11y-announcer\"></div>\n<div class=\"screen-reader-info\">\n<h3>Screen Reader Output</h3>\n<p>A screen reader would announce the following for this component:</p>\n<div class=\"reader-output\" id=\"reader-output\">\n<div class=\"output-entry\">Tab list with 4 tabs</div>\n</div>\n</div>\n</div>\nCSS\n:root {\n--color-primary: #4a6cf7;\n--color-primary-hover: #3a5cf7;\n--color-primary-light: #e0e7ff;\n--color-text: #1f2937;\n--color-text-light: #6b7280;\n--color-border: #e5e7eb;\n--color-surface: #ffffff;\n--color-background: #f9fafb;\n--color-focus: #3b82f6;\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n--border-radius: 6px;\n--focus-ring: 0 0 0 3px rgba(59, 130, 246, 0.5);\n--transition-fast: 150ms ease;\n}\n* {\nbox-sizing: border-box;\n}\nbody {\nfont-family: var(--font-family);\nline-height: 1.5;\ncolor: var(--color-text);\nbackground-color: var(--color-background);\nmargin: 0;\npadding: 20px;\n}\n.container {\nmax-width: 800px;\nmargin: 0 auto;\n}\nh2 {\nfont-size: 24px;\nmargin-top: 0;\nmargin-bottom: 24px;\n}\nh3 {\nfont-size: 18px;\nmargin-top: 0;\nmargin-bottom: 12px;\n}\n.intro-box {\nbackground-color: #e0f2fe;\nborder-left: 4px solid var(--color-focus);\npadding: 16px;\nmargin-bottom: 20px;\nborder-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n.intro-box ul {\nmargin: 0;\npadding-left: 24px;\n}\n.keyboard-instructions {\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nmargin-bottom: 20px;\nbox-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n.keyboard-instructions ul {\nmargin: 0;\npadding-left: 24px;\n}\nkbd {\nbackground-color: #f1f5f9;\nborder: 1px solid #cbd5e1;\nborder-radius: 3px;\nbox-shadow: 0 1px 1px rgba(0,0,0,0.2);\npadding: 2px 6px;\nfont-size: 12px;\nfont-family: monospace;\n}\n.component-container {\nmargin-bottom: 30px;\nborder: 1px dashed #cbd5e1;\npadding: 20px;\nborder-radius: var(--border-radius);\n}\n.live-region {\nposition: absolute;\nwidth: 1px;\nheight: 1px;\npadding: 0;\nmargin: -1px;\noverflow: hidden;\nclip: rect(0, 0, 0, 0);\nwhite-space: nowrap;\nborder-width: 0;\n}\n.screen-reader-info {\nbackground-color: var(--color-surface);\npadding: 16px;\nborder-radius: var(--border-radius);\nmargin-top: 20px;\nbox-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n.reader-output {\nbackground-color: #f1f5f9;\nborder: 1px solid #cbd5e1;\nborder-radius: 4px;\npadding: 12px;\nfont-family: monospace;\nmax-height: 200px;\noverflow-y: auto;\n}\n.output-entry {\nmargin-bottom: 8px;\npadding-bottom: 8px;\nborder-bottom: 1px solid #e2e8f0;\n}\n.output-entry:last-child {\nmargin-bottom: 0;\npadding-bottom: 0;\nborder-bottom: 0;\n}\nJavaScript\n// Helper function to simulate screen reader announcements\nfunction announceToScreenReader(message) {\n// Announce to actual screen readers via aria-live\nconst liveRegion = document.getElementById('a11y-announcer');\nif (liveRegion) {\nliveRegion.textContent = message;\n}\n// Also show in our reader output visualization\nconst readerOutput = document.getElementById('reader-output');\nif (readerOutput) {\nconst entry = document.createElement('div');\nentry.className = 'output-entry';\nentry.textContent = message;\nreaderOutput.appendChild(entry);\n// Auto-scroll to bottom\nreaderOutput.scrollTop = readerOutput.scrollHeight;\n}\n}\n// Define the AccessibleTabPanel component\nclass AccessibleTabPanel extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Component state\nthis._tabs = [];\nthis._panels = [];\nthis._selectedIndex = 0;\n// Initial render\nthis.render();\n}\nconnectedCallback() {\n// Process slots when component is connected to DOM\nthis.shadowRoot.addEventListener('slotchange', this._processSlots.bind(this));\n// Initial processing if slots are already populated\nthis._processSlots();\n// Announce to screen reader that component is ready\nannounceToScreenReader('Tab panel component loaded with 4 tabs');\n}\n// Process tab and panel slots\n_processSlots() {\nconst tabSlot = this.shadowRoot.querySelector('slot[name=\"tab\"]');\nconst panelSlot = this.shadowRoot.querySelector('slot[name=\"panel\"]');\nif (tabSlot && panelSlot) {\n// Get assigned elements\nthis._tabs = tabSlot.assignedElements();\nthis._panels = panelSlot.assignedElements();\n// Set up tabs and panels\nif (this._tabs.length > 0 && this._panels.length > 0) {\nthis._setupTabsAndPanels();\nthis._selectTab(this._selectedIndex);\n}\n}\n}\n// Set up tabs and panels with proper attributes and event listeners\n_setupTabsAndPanels() {\nconst tablist = this.shadowRoot.querySelector('[role=\"tablist\"]');\n// Configure tabs\nthis._tabs.forEach((tab, index) => {\n// Set tab attributes\ntab.setAttribute('role', 'tab');\ntab.setAttribute('id', `tab-${this.id}-${index}`);\ntab.setAttribute('aria-selected', index === this._selectedIndex ? 'true' : 'false');\ntab.setAttribute('tabindex', index === this._selectedIndex ? '0' : '-1');\ntab.setAttribute('aria-controls', `panel-${this.id}-${index}`);\n// Add click event listener\ntab.addEventListener('click', () => this._selectTab(index));\n// Add keydown event listener for keyboard navigation\ntab.addEventListener('keydown', e => this._handleTabKeyDown(e, index));\n});\n// Configure panels\nthis._panels.forEach((panel, index) => {\n// Set panel attributes\npanel.setAttribute('role', 'tabpanel');\npanel.setAttribute('id', `panel-${this.id}-${index}`);\npanel.setAttribute('aria-labelledby', `tab-${this.id}-${index}`);\npanel.setAttribute('tabindex', '0'); // Make panel focusable\npanel.hidden = index !== this._selectedIndex;\n});\n// Add keydown event listener to tablist for keyboard navigation\ntablist.addEventListener('keydown', this._handleTablistKeyDown.bind(this));\n}\n// Handle keydown events on the tablist\n_handleTablistKeyDown(event) {\n// This is a fallback for the whole tablist, individual tabs have their own handlers\nif (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\nevent.preventDefault(); // Prevent scrolling\n}\n}\n// Handle keydown events on individual tabs\n_handleTabKeyDown(event, index) {\nlet newIndex = index;\nswitch (event.key) {\ncase 'ArrowRight':\nevent.preventDefault();\nnewIndex = (index + 1) % this._tabs.length;\nthis._tabs[newIndex].focus();\nthis._selectTab(newIndex);\nbreak;\ncase 'ArrowLeft':\nevent.preventDefault();\nnewIndex = (index - 1 + this._tabs.length) % this._tabs.length;\nthis._tabs[newIndex].focus();\nthis._selectTab(newIndex);\nbreak;\ncase 'Home':\nevent.preventDefault();\nthis._tabs[0].focus();\nthis._selectTab(0);\nbreak;\ncase 'End':\nevent.preventDefault();\nnewIndex = this._tabs.length - 1;\nthis._tabs[newIndex].focus();\nthis._selectTab(newIndex);\nbreak;\ncase 'Enter':\ncase ' ':\nevent.preventDefault();\nthis._selectTab(index);\nbreak;}\n}\n// Select a tab and show its panel\n_selectTab(index) {\nif (index < 0 || index >= this._tabs.length) return;\n// Update selected tab\nthis._tabs.forEach((tab, i) => {\nconst selected = i === index;\ntab.setAttribute('aria-selected', selected ? 'true' : 'false');\ntab.setAttribute('tabindex', selected ? '0' : '-1');\n// Apply selected styling\ntab.classList.toggle('selected', selected);\n});\n// Update visible panel\nthis._panels.forEach((panel, i) => {\npanel.hidden = i !== index;\n});\n// Update state\nthis._selectedIndex = index;\n// Announce tab change to screen reader\nconst tabName = this._tabs[index].textContent.trim();\nannounceToScreenReader(`Selected tab: ${tabName}`);\n}\n// Render the component\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.tabs-container {\ndisplay: flex;\nflex-direction: column;\n}\n.tablist {\ndisplay: flex;\nborder-bottom: 1px solid var(--color-border, #e5e7eb);\nmargin-bottom: 20px;\n}\n::slotted([slot=\"tab\"]) {\npadding: 10px 20px;\nborder: 1px solid transparent;\nborder-bottom: none;\nborder-top-left-radius: 4px;\nborder-top-right-radius: 4px;\ncursor: pointer;\nmargin-right: 4px;\nmargin-bottom: -1px;\nbackground-color: var(--color-background, #f9fafb);\nposition: relative;\ntransition: all var(--transition-fast, 150ms ease);\n}\n::slotted([slot=\"tab\"]:hover) {\nbackground-color: var(--color-primary-light, #e0e7ff);\n}\n::slotted([slot=\"tab\"].selected) {\nbackground-color: var(--color-surface, #ffffff);\nborder-color: var(--color-border, #e5e7eb);\nborder-bottom-color: var(--color-surface, #ffffff);\ncolor: var(--color-primary, #4a6cf7);\nfont-weight: 500;\n}\n/* Focus styles */\n::slotted([slot=\"tab\"]:focus-visible) {\noutline: none;\nbox-shadow: var(--focus-ring, 0 0 0 3px rgba(59, 130, 246, 0.5));\nposition: relative;\nz-index: 1;\n}\n/* Prevent focus ring AND selected styles overlapping */\n::slotted([slot=\"tab\"].selected:focus-visible) {\nborder-bottom-color: var(--color-primary-light, #e0e7ff);\n}\n.panels-container {\nmin-height: 200px;\nbackground-color: var(--color-surface, #ffffff);\nborder-radius: 0 0 var(--border-radius, 6px) var(--border-radius, 6px);\npadding: 16px 0;\n}\n::slotted([slot=\"panel\"]) {\nmargin: 0;\npadding: 0 16px;\n}\n/* Focus styles for panels */\n::slotted([slot=\"panel\"]:focus-visible) {\noutline: none;\nbox-shadow: 0 0 0 2px var(--color-focus, #3b82f6);\nborder-radius: var(--border-radius, 6px);\n}\n</style>\n<div class=\"tabs-container\">\n<div class=\"tablist\" role=\"tablist\" aria-label=\"Programming topics\">\n<slot name=\"tab\"></slot>\n</div>\n<div class=\"panels-container\">\n<slot name=\"panel\"></slot>\n</div>\n</div>\n`;\n}}\n// Register the custom element\ncustomElements.define('accessible-tab-panel', AccessibleTabPanel);\n// Initialize the demo\ndocument.addEventListener('DOMContentLoaded', () => {\n// Set up demonstration features\nconst tabPanel = document.getElementById('demo-tabs');\n// Ensure the component is initialized\nif (tabPanel) {\n// Listen for tab changes for demo purposes\ntabPanel.addEventListener('keydown', e => {\nif (['ArrowRight', 'ArrowLeft', 'Home', 'End'].includes(e.key)) {\nannounceToScreenReader(`Navigated with ${e.key} key`);\n}\n});\n}\n});\nAccessible Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis accessible tab panel implementation showcases several key accessibility features:\nProper ARIA roles and attributes - Using role=\"tablist\", role=\"tab\", and role=\"tabpanel\" with appropriate relationships\nKeyboard navigation - Arrow keys to move between tabs, Home/End to jump to first/last tab\nFocus management - Visible focus indicators and proper focus order\nScreen reader support - Appropriate announcements when tabs change\nTry navigating the tabs using only your keyboard. Press Tab to focus on the tab list, then use arrow keys to move between tabs. Notice how the focus is visually indicated and how the selected tab's content is displayed.\nThis implementation follows the WAI-ARIA Authoring Practices (https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) for tab panels, (https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/) ensuring that users of assistive technologies can effectively interact with the component.\nIntegration with Frameworks\nMany organizations need to integrate Web Components with existing framework codebases:\nReact Integration\n// React wrapper around a Web Component\nimport React, { useRef, useEffect } from 'react';\nimport 'my-web-components/dist/data-chart.js';\nfunction DataChartWrapper(props) {\nconst chartRef = useRef(null);\nuseEffect(() => {\n// Pass complex data as a property\nchartRef.current.data = props.data;\n}, [props.data]);\nuseEffect(() => {\n// Custom events: attach the listener yourself\nconst el = chartRef.current;\nconst handler = (e) => props.onUpdate?.(e.detail);\nel.addEventListener('chart-update', handler);\nreturn () => el.removeEventListener('chart-update', handler);\n}, [props.onUpdate]);\n// Simple values go in as attributes\nreturn <data-chart ref={chartRef} theme={props.theme} height={props.height} />;\n}\nNote the listener: writing onChartUpdate={props.onUpdate} on the element does not attach a listener for a custom event in React 18 and earlier, and React 19 is not covered here. addEventListener on the ref works in every version.\nTo see how React and Web Components work together in practice, here's an interactive demo showing the integration patterns in action:\nHTML\n<div id=\"react-app\"></div>\nCSS\n:root {\n--color-primary: #4a6cf7;\n--color-primary-light: #e0e7ff;\n--color-surface: #ffffff;\n--color-background: #f9fafb;\n--color-border: #e5e7eb;\n--color-text: #1f2937;\n--color-text-light: #6b7280;\n--color-success: #10b981;\n--color-warning: #f59e0b;\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n--border-radius: 6px;\n--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);\n--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\n}\nbody {\nfont-family: var(--font-family);\nmargin: 0;\npadding: 16px;\nbackground-color: var(--color-background);\ncolor: var(--color-text);\n}\n.app-container {\nmax-width: 800px;\nmargin: 0 auto;\n}\n.header {\nmargin-bottom: 24px;\n}\nh1 {\nfont-size: 24px;\nmargin-top: 0;\nmargin-bottom: 8px;\n}\n.intro {\nbackground-color: var(--color-primary-light);\nborder-left: 4px solid var(--color-primary);\npadding: 16px;\nmargin-bottom: 24px;\nborder-radius: 0 var(--border-radius) var(--border-radius) 0;\n}\n.integration-demo {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 24px;\nmargin-bottom: 24px;\n}\n@media (max-width: 700px) {\n.integration-demo {\ngrid-template-columns: 1fr;\n}\n}\n.react-side, .webcomponent-side {\nbackground-color: var(--color-surface);\nborder-radius: var(--border-radius);\nbox-shadow: var(--shadow-sm);\npadding: 20px;\n}\n.side-label {\ndisplay: inline-block;\nbackground-color: #f1f5f9;\ncolor: var(--color-text-light);\nfont-size: 12px;\npadding: 4px 8px;\nborder-radius: 4px;\nmargin-bottom: 16px;\n}\n.section-title {\nfont-size: 18px;\nmargin-top: 0;\nmargin-bottom: 16px;\nfont-weight: 600;\n}\n.control-group {\nmargin-bottom: 16px;\n}\n.control-group label {\ndisplay: block;\nmargin-bottom: 8px;\nfont-weight: 500;\n}\n.color-options {\ndisplay: flex;\ngap: 8px;\nmargin-bottom: 16px;\n}\n.color-option {\nwidth: 32px;\nheight: 32px;\nborder-radius: 4px;\ncursor: pointer;\nposition: relative;\ntransition: transform 0.2s;\n}\n.color-option:hover {\ntransform: scale(1.1);\n}\n.color-option.selected::after {\ncontent: \"✓\";\nposition: absolute;\ntop: 50%;\nleft: 50%;\ntransform: translate(-50%, -50%);\ncolor: white;\ntext-shadow: 0 0 2px rgba(0, 0, 0, 0.5);\n}\nbutton {\nbackground-color: var(--color-primary);\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\nfont-family: inherit;\nfont-size: 14px;\ncursor: pointer;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #3a5cf7;\n}\n.counter-display {\nfont-size: 24px;\nfont-weight: 600;\nmargin-bottom: 16px;\ncolor: var(--color-primary);\n}\n.message-display {\nmargin-top: 16px;\nmargin-bottom: 16px;\npadding: 12px;\nbackground-color: #f1f5f9;\nborder-radius: 4px;\nfont-family: monospace;\n}\n.event-flow {\nposition: relative;\nheight: 120px;\nmargin-top: 24px;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n.event-arrow {\nwidth: 80%;\nheight: 2px;\nbackground-color: var(--color-text-light);\nposition: relative;\nmargin: 0 auto;\n}\n.event-arrow::before, .event-arrow::after {\ncontent: \"\";\nposition: absolute;\ntop: -4px;\nwidth: 10px;\nheight: 10px;\nborder-radius: 50%;\nbackground-color: var(--color-text-light);\n}\n.event-arrow::before {\nleft: 0;\n}\n.event-arrow::after {\nright: 0;\n}\n.event-label {\nposition: absolute;\ntop: 10px;\nleft: 50%;\ntransform: translateX(-50%);\nbackground-color: var(--color-surface);\npadding: 4px 8px;\nfont-size: 12px;\nborder-radius: 4px;\nbox-shadow: var(--shadow-sm);\nwhite-space: nowrap;\n}\n.event-trigger .event-label {\ntop: auto;\nbottom: 10px;\n}\n.event-pulse {\nposition: absolute;\ntop: -4px;\nleft: 50%;\ntransform: translateX(-50%);\nwidth: 10px;\nheight: 10px;\nbackground-color: var(--color-success);\nborder-radius: 50%;\n}\n.pulse-animation {\nanimation: pulse 1s linear;\n}\n@keyframes pulse {\n0% {\nbox-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);\n}\n70% {\nbox-shadow: 0 0 0 15px rgba(16, 185, 129, 0);\n}\n100% {\nbox-shadow: 0 0 0 0 rgba(16, 185, 129, 0);\n}\n}\n.code-section {\nbackground-color: var(--color-surface);\nborder-radius: var(--border-radius);\npadding: 20px;\nmargin-bottom: 24px;\nbox-shadow: var(--shadow-sm);\n}\n.code-tabs {\ndisplay: flex;\nborder-bottom: 1px solid var(--color-border);\nmargin-bottom: 16px;\n}\n.code-tab {\npadding: 8px 16px;\ncursor: pointer;\nposition: relative;\nuser-select: none;\n}\n.code-tab.active {\nfont-weight: 600;\ncolor: var(--color-primary);\n}\n.code-tab.active::after {\ncontent: \"\";\nposition: absolute;\nbottom: -1px;\nleft: 0;\nright: 0;\nheight: 2px;\nbackground-color: var(--color-primary);\n}\n.code-content {\nposition: relative;\n}\npre {\nbackground-color: #f8fafc;\npadding: 16px;\nborder-radius: 4px;\noverflow-x: auto;\nmargin: 0;\n}\ncode {\nfont-family: monospace;\nfont-size: 14px;\nline-height: 1.5;\n}\n/* Counter component custom styles */\ncounter-element {\ndisplay: block;\nborder-radius: var(--border-radius);\npadding: 20px;\nmargin-bottom: 16px;\ntransition: all 0.3s ease;\n}\nJavaScript\nclass CounterElement extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Initialize properties with default values\nthis._count = 0;\nthis._color = '#4a6cf7';\n// Render initial state\nthis.render();\n}\n// Define which attributes to observe\nstatic get observedAttributes() {\nreturn ['count', 'color'];\n}\n// Handle attribute changes\nattributeChangedCallback(name, oldValue, newValue) {\nif (oldValue === newValue) return;\nif (name === 'count') {\nthis._count = parseInt(newValue, 10);\n} else if (name === 'color') {\nthis._color = newValue;\n}\nthis.render();\n}\n// Getter/setter for count property\nget count() {\nreturn this._count;\n}\nset count(value) {\nthis._count = value;\nthis.setAttribute('count', value);\n}\n// Getter/setter for color property\nget color() {\nreturn this._color;\n}\nset color(value) {\nthis._color = value;\nthis.setAttribute('color', value);\n}\n// Handle click event\nhandleIncrement() {\nthis._count++;\nthis.setAttribute('count', this._count);\n// Dispatch custom event\nthis.dispatchEvent(new CustomEvent('counter-incremented', {\nbubbles: true,\ncomposed: true,\ndetail: { count: this._count } }));\n}\n// Render the component\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui, sans-serif);\n}\n.counter-container {\nbackground-color: ${this._color}15;\nborder: 2px solid ${this._color};\nborder-radius: 8px;\npadding: 16px;\ntext-align: center;\ntransition: all 0.3s ease;\n}\n.counter-value {\nfont-size: 32px;\nfont-weight: bold;\ncolor: ${this._color};\nmargin-bottom: 16px;\n}\nbutton {\nbackground-color: ${this._color};\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 16px;\nfont-size: 16px;\ncursor: pointer;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nfilter: brightness(110%);\n}\n.counter-label {\nmargin-top: 8px;\nfont-size: 14px;\ncolor: #6b7280;\n}\n</style>\n<div class=\"counter-container\">\n<div class=\"counter-value\">${this._count}</div>\n<button id=\"increment-btn\">Increment from Web Component</button>\n<div class=\"counter-label\">Web Component Internal Counter</div>\n</div>\n`;\n// Add event listener to button\nthis.shadowRoot.getElementById('increment-btn').addEventListener('click', this.handleIncrement.bind(this));\n}\n// Clean up when element is removed\ndisconnectedCallback() {var _this$shadowRoot$getE;\n// Remove event listeners if needed\n(_this$shadowRoot$getE = this.shadowRoot.getElementById('increment-btn')) === null || _this$shadowRoot$getE === void 0 ? void 0 : _this$shadowRoot$getE.removeEventListener('click', this.handleIncrement);\n}}\n// Register custom element\ncustomElements.define('counter-element', CounterElement);\n// Load React and ReactDOM from CDN\nfunction loadReactDependencies() {\nreturn new Promise((resolve, reject) => {\n// Load React\nconst reactScript = document.createElement('script');\nreactScript.src = 'https://unpkg.com/react@18/umd/react.production.min.js';\nreactScript.crossOrigin = true;\ndocument.head.appendChild(reactScript);\n// Load ReactDOM\nconst reactDOMScript = document.createElement('script');\nreactDOMScript.src = 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js';\nreactDOMScript.crossOrigin = true;\nreactDOMScript.onload = resolve;\nreactDOMScript.onerror = reject;\ndocument.head.appendChild(reactDOMScript);\n});\n}\n// Load Babel for JSX support\nfunction loadBabel() {\nreturn new Promise((resolve, reject) => {\nconst babelScript = document.createElement('script');\nbabelScript.src = 'https://unpkg.com/@babel/standalone/babel.min.js';\nbabelScript.onload = resolve;\nbabelScript.onerror = reject;\ndocument.head.appendChild(babelScript);\n});\n}\n// Load dependencies and then initialize app\nasync function initApp() {\n// Create and show loader\nconst loader = document.createElement('div');\nloader.id = 'app-loader';\nloader.innerHTML = `\n<style>\n#app-loader {\nposition: fixed;\ntop: 0;\nleft: 0;\nwidth: 100%;\nheight: 100%;\ndisplay: flex;\nflex-direction: column;\njustify-content: center;\nalign-items: center;\nbackground-color: rgba(255, 255, 255, 0.9);\nz-index: 1000;\nfont-family: system-ui, sans-serif;\n}\n.spinner {\nwidth: 50px;\nheight: 50px;\nborder: 5px solid #f3f3f3;\nborder-top: 5px solid #4a6cf7;\nborder-radius: 50%;\nanimation: spin 1s linear infinite;\nmargin-bottom: 20px;\n}\n.loading-text {\ncolor: #333;\nfont-size: 18px;\n}\n@keyframes spin {\n0% { transform: rotate(0deg); }\n100% { transform: rotate(360deg); }\n}\n</style>\n<div class=\"spinner\"></div>\n<div class=\"loading-text\">Loading application...</div>\n`;\ndocument.body.appendChild(loader);\ntry {\n// React is provided by the sandbox (libs=react); the JSX below is already compiled.\n// Add a delay to make sure everything is loaded\nsetTimeout(() => {\n// Remove loader when rendering is complete\ndocument.body.removeChild(loader);\nrenderReactApp();\n}, 300);\n} catch (error) {\nconsole.error('Failed to load dependencies:', error);\n// Update loader to show error\nloader.innerHTML = `\n<style>\n#app-loader {\nposition: fixed;\ntop: 0;\nleft: 0;\nwidth: 100%;\nheight: 100%;\ndisplay: flex;\nflex-direction: column;\njustify-content: center;\nalign-items: center;\nbackground-color: rgba(255, 255, 255, 0.9);\nz-index: 1000;\nfont-family: system-ui, sans-serif;\n}\n.error-icon {\nfont-size: 50px;\ncolor: #ef4444;\nmargin-bottom: 20px;\n}\n.error-text {\ncolor: #333;\nfont-size: 18px;\nmax-width: 80%;\ntext-align: center;\n}\n</style>\n<div class=\"error-icon\">❌</div>\n<div class=\"error-text\">Failed to load application. Please refresh the page or try again later.</div>\n`;\n}\n}\n// Render React app\nfunction renderReactApp() {\n// Define the React component\nconst App = () => {var _colors$find;\nconst [count, setCount] = React.useState(0);\nconst [color, setColor] = React.useState('#4a6cf7');\nconst [lastEvent, setLastEvent] = React.useState(null);\nconst counterRef = React.useRef(null);\nconst [showPulse, setShowPulse] = React.useState(false);\n// Available colors\nconst colors = [\n{ value: '#4a6cf7', name: 'Blue' },\n{ value: '#10b981', name: 'Green' },\n{ value: '#f59e0b', name: 'Amber' },\n{ value: '#ef4444', name: 'Red' },\n{ value: '#8b5cf6', name: 'Purple' }];\n// Handle color change\nconst handleColorChange = newColor => {\nsetColor(newColor);\n};\n// Handle increment from React\nconst handleIncrement = () => {\nsetCount(prevCount => prevCount + 1);\n};\n// Handle event from web component\nconst handleCounterIncremented = event => {\nconst newCount = event.detail.count;\nsetCount(newCount);\nsetLastEvent(`counter-incremented event received: count=${newCount}`);\n// Show pulse animation\nsetShowPulse(true);\nsetTimeout(() => setShowPulse(false), 1000);\n};\n// Effect to set properties on web component when React state changes\nReact.useEffect(() => {\nif (counterRef.current) {\n// Set properties directly\ncounterRef.current.count = count;\ncounterRef.current.color = color;\n}\n}, [count, color]);\n// Effect to add event listeners on component mount\nReact.useEffect(() => {\nconst counter = counterRef.current;\nif (counter) {\ncounter.addEventListener('counter-incremented', handleCounterIncremented);\n// Cleanup\nreturn () => {\ncounter.removeEventListener('counter-incremented', handleCounterIncremented);\n};\n}\n}, []);\n// Get selected color name\nconst selectedColorName = ((_colors$find = colors.find(c => c.value === color)) === null || _colors$find === void 0 ? void 0 : _colors$find.name) || 'Blue';\nreturn /*#__PURE__*/(\nReact.createElement(\"div\", { className: \"app-container\" }, /*#__PURE__*/\nReact.createElement(\"div\", { className: \"header\" }, /*#__PURE__*/\nReact.createElement(\"h1\", null, \"React + Web Components Integration\")), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"intro\" }, /*#__PURE__*/\nReact.createElement(\"p\", null, \"This demo shows how React can interact with Web Components. The React app sets properties on the Web Component through direct property access, and listens for events coming from the Web Component.\")), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"integration-demo\" }, /*#__PURE__*/\nReact.createElement(\"div\", { className: \"react-side\" }, /*#__PURE__*/\nReact.createElement(\"span\", { className: \"side-label\" }, \"React App\"), /*#__PURE__*/\nReact.createElement(\"h2\", { className: \"section-title\" }, \"React Controls\"), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"control-group\" }, /*#__PURE__*/\nReact.createElement(\"label\", null, \"Color Theme:\"), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"color-options\" },\ncolors.map((colorObj) => /*#__PURE__*/\nReact.createElement(\"div\", {\nkey: colorObj.value,\nclassName: `color-option ${color === colorObj.value ? 'selected' : ''}`,\nstyle: { backgroundColor: colorObj.value },\ntitle: colorObj.name,\nonClick: () => handleColorChange(colorObj.value) })))), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"counter-display\" }, \"Count: \",\ncount), /*#__PURE__*/\nReact.createElement(\"button\", { onClick: handleIncrement }, \"Increment from React\"),\nlastEvent && /*#__PURE__*/\nReact.createElement(\"div\", { className: \"message-display\" },\nlastEvent)), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"webcomponent-side\" }, /*#__PURE__*/\nReact.createElement(\"span\", { className: \"side-label\" }, \"Web Component\"), /*#__PURE__*/\nReact.createElement(\"h2\", { className: \"section-title\" }, \"Counter Element\"), /*#__PURE__*/\nReact.createElement(\"counter-element\", {\nref: counterRef,\ncount: count,\ncolor: color,\nstyle: { backgroundColor: `${color}15` } }), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"component-info\" }, /*#__PURE__*/\nReact.createElement(\"div\", null, /*#__PURE__*/React.createElement(\"strong\", null, \"Current properties:\")), /*#__PURE__*/\nReact.createElement(\"div\", null, \"count: \", count), /*#__PURE__*/\nReact.createElement(\"div\", null, \"color: \", selectedColorName)))), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"event-flow\" }, /*#__PURE__*/\nReact.createElement(\"div\", { className: \"event-arrow\" }, /*#__PURE__*/\nReact.createElement(\"div\", { className: `event-pulse ${showPulse ? 'pulse-animation' : ''}` })), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"event-label\" }, \"Web Component \\u2192 React\"), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"event-label event-trigger\" }, \"React \\u2192 Web Component\")), /*#__PURE__*/\nReact.createElement(\"div\", { className: \"code-section\" }, /*#__PURE__*/\nReact.createElement(\"div\", { className: \"section-title\" }, \"Integration Code\"), /*#__PURE__*/\nReact.createElement(\"pre\", null, /*#__PURE__*/\nReact.createElement(\"code\", null, `// React Component\nfunction CounterWrapper() {\nconst [count, setCount] = React.useState(0);\nconst counterRef = React.useRef(null);\n// Handle event from web component\nconst handleCounterIncremented = (event) => {\nsetCount(event.detail.count);\n};\n// Effect to set up event listener\nReact.useEffect(() => {\nconst counter = counterRef.current;\nif (counter) {\ncounter.addEventListener(\n'counter-incremented',\nhandleCounterIncremented\n);\nreturn () => {\ncounter.removeEventListener(\n'counter-incremented',\nhandleCounterIncremented\n);\n};\n}\n}, []);\n// Update property when React state changes\nReact.useEffect(() => {\nif (counterRef.current) {\ncounterRef.current.count = count;\n}\n}, [count]);\nreturn (\n<div>\n<counter-element ref={counterRef}></counter-element>\n<button onClick={() => setCount(count + 1)}>\nIncrement\n</button>\n</div>\n);\n}`)))));\n};\nconst container = document.getElementById('react-app');\nReactDOM.render( /*#__PURE__*/React.createElement(App, null), container);\n}\n// Initialize the application\ninitApp();\nFramework Integration Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis demo showcases the key integration patterns described earlier:\nUsing refs to access Web Component properties - React uses a ref to directly set properties on the Web Component\nEvent-based communication - The Web Component dispatches custom events that React listens for\nBidirectional data flow - Changes in either React or the Web Component are synchronized\nTry changing the color or clicking the buttons on either side to see how React and the Web Component communicate. Notice how property changes flow from React to the Web Component, while events flow from the Web Component back to React.\nMicro-Frontend Architectures with Web Components\nMicro-frontends split one page into parts that separate teams build and ship. Web components are one way to give each part a boundary:\n<!-- Shell application -->\n<html>\n<head>\n<title>Banking Dashboard</title>\n<!-- Import shared components -->\n<script type=\"module\" src=\"/shared/components/ing-header.js\"></script>\n<script type=\"module\" src=\"/shared/components/ing-footer.js\"></script>\n</head>\n<body>\n<!-- Shared header component owned by core team -->\n<ing-header user-id=\"12345\"></ing-header>\n<!-- Micro-frontend container -->\n<main>\n<!-- Accounts micro-frontend owned by accounts team -->\n<accounts-overview></accounts-overview>\n<!-- Investments micro-frontend owned by investments team -->\n<investments-summary></investments-summary>\n<!-- Loans micro-frontend owned by loans team -->\n<loans-overview></loans-overview>\n</main>\n<!-- Shared footer component -->\n<ing-footer></ing-footer>\n<!-- Load micro-frontends -->\n<script type=\"module\" src=\"/accounts/components/accounts-overview.js\"></script>\n<script type=\"module\" src=\"/investments/components/investments-summary.js\"></script>\n<script type=\"module\" src=\"/loans/components/loans-overview.js\"></script>\n</body>\n</html>\nThis approach allows different teams to develop and deploy independently while maintaining a cohesive user experience.\nLet's visualize how a micro-frontend architecture might work in practice with this interactive demo:\nHTML\n<div class=\"app-container\">\n<header class=\"shell-header core-team\">\n<div class=\"team-tag\">Core Team</div>\n<div class=\"shell-logo\">Banking Portal</div>\n<div class=\"user-info\">\n<span class=\"user-name\">Jane Doe</span>\n<span class=\"user-avatar\">JD</span>\n</div>\n</header>\n<nav class=\"shell-nav core-team\">\n<div class=\"team-tag\">Core Team</div>\n<ul class=\"nav-items\">\n<li class=\"nav-item active\" data-section=\"accounts\">Accounts</li>\n<li class=\"nav-item\" data-section=\"investments\">Investments</li>\n<li class=\"nav-item\" data-section=\"loans\">Loans</li>\n<li class=\"nav-item\" data-section=\"support\">Support</li>\n</ul>\n</nav>\n<div class=\"content-area\">\n<section id=\"microfrontend-accounts\" class=\"accounts-team active\">\n<div class=\"team-tag\">Accounts Team</div>\n<accounts-dashboard></accounts-dashboard>\n</section>\n<section id=\"microfrontend-investments\" class=\"investments-team\">\n<div class=\"team-tag\">Investments Team</div>\n<investments-summary></investments-summary>\n</section>\n<section id=\"microfrontend-loans\" class=\"loans-team\">\n<div class=\"team-tag\">Loans Team</div>\n<loans-overview></loans-overview>\n</section>\n<section id=\"microfrontend-support\" class=\"support-team\">\n<div class=\"team-tag\">Support Team</div>\n<support-widget></support-widget>\n</section>\n</div>\n<footer class=\"shell-footer core-team\">\n<div class=\"team-tag\">Core Team</div>\n<div class=\"footer-content\">\n<p>© 2025 Demo Banking Portal | Privacy Policy | Terms of Service</p>\n</div>\n</footer>\n<div class=\"event-log\">\n<h3>Cross-Team Communication Log</h3>\n<div id=\"event-log-content\"></div>\n</div>\n<div class=\"architecture-diagram\">\n<h3>Micro-Frontend Architecture</h3>\n<div class=\"diagram\">\n<div class=\"diagram-shell\">\n<div class=\"diagram-label\">Shell Application (Core Team)</div>\n<div class=\"diagram-container\">\n<div class=\"diagram-header\"></div>\n<div class=\"diagram-nav\"></div>\n<div class=\"diagram-content\">\n<div class=\"diagram-mfe accounts\"></div>\n<div class=\"diagram-mfe investments\"></div>\n<div class=\"diagram-mfe loans\"></div>\n<div class=\"diagram-mfe support\"></div>\n</div>\n<div class=\"diagram-footer\"></div>\n</div>\n</div>\n</div>\n</div>\n</div>\nCSS\n:root {\n/* Core Team Colors */\n--core-team-color: #3b82f6;\n--core-team-light: #eff6ff;\n/* Accounts Team Colors */\n--accounts-team-color: #10b981;\n--accounts-team-light: #ecfdf5;\n/* Investments Team Colors */\n--investments-team-color: #8b5cf6;\n--investments-team-light: #f5f3ff;\n/* Loans Team Colors */\n--loans-team-color: #f59e0b;\n--loans-team-light: #fffbeb;\n/* Support Team Colors */\n--support-team-color: #ef4444;\n--support-team-light: #fef2f2;\n/* UI Colors */\n--surface-color: #ffffff;\n--border-color: #e5e7eb;\n--text-color: #111827;\n--text-secondary: #6b7280;\n--background-color: #f9fafb;\n/* Dimensions */\n--header-height: 60px;\n--footer-height: 60px;\n--nav-width: 200px;\n/* Font */\n--font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n}\n* {\nbox-sizing: border-box;\nmargin: 0;\npadding: 0;\n}\nbody {\nfont-family: var(--font-family);\nbackground-color: var(--background-color);\ncolor: var(--text-color);\nline-height: 1.5;\nmargin: 0;\npadding: 0;\n}\n.app-container {\nmax-width: 1200px;\nmargin: 0 auto;\npadding: 20px;\n}\n/* Team tags */\n.team-tag {\nposition: absolute;\ntop: 0;\nright: 0;\nfont-size: 11px;\npadding: 2px 6px;\nborder-radius: 0 0 0 4px;\ncolor: white;\nfont-weight: 500;\nopacity: 0.9;\nz-index: 10;\n}\n.core-team .team-tag {\nbackground-color: var(--core-team-color);\n}\n.accounts-team .team-tag {\nbackground-color: var(--accounts-team-color);\n}\n.investments-team .team-tag {\nbackground-color: var(--investments-team-color);\n}\n.loans-team .team-tag {\nbackground-color: var(--loans-team-color);\n}\n.support-team .team-tag {\nbackground-color: var(--support-team-color);\n}\n/* Shell Header */\n.shell-header {\nposition: relative;\ndisplay: flex;\nalign-items: center;\njustify-content: space-between;\npadding: 0 20px;\nheight: var(--header-height);\nbackground-color: var(--surface-color);\nborder-bottom: 1px solid var(--border-color);\nmargin-bottom: 20px;\nborder-radius: 8px;\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n.shell-logo {\nfont-size: 20px;\nfont-weight: 600;\ncolor: var(--core-team-color);\n}\n.user-info {\ndisplay: flex;\nalign-items: center;\ngap: 10px;\n}\n.user-name {\nfont-weight: 500;\n}\n.user-avatar {\ndisplay: flex;\nalign-items: center;\njustify-content: center;\nwidth: 32px;\nheight: 32px;\nbackground-color: var(--core-team-color);\ncolor: white;\nborder-radius: 50%;\nfont-weight: 500;\n}\n/* Shell Navigation and Content Layout */\n.content-area {\ndisplay: flex;\nmargin-bottom: 20px;\nmin-height: 400px;\n}\n.shell-nav {\nposition: relative;\nwidth: var(--nav-width);\nbackground-color: var(--surface-color);\nborder-radius: 8px;\nmargin-right: 20px;\npadding: 20px 0;\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\nflex-shrink: 0;\n}\n.nav-items {\nlist-style: none;\nmargin-top: 20px;\n}\n.nav-item {\npadding: 10px 20px;\nmargin-bottom: 5px;\ncursor: pointer;\nborder-left: 3px solid transparent;\ntransition: all 0.2s ease;\n}\n.nav-item:hover {\nbackground-color: var(--core-team-light);\n}\n.nav-item.active {\nbackground-color: var(--core-team-light);\nborder-left-color: var(--core-team-color);\nfont-weight: 500;\n}\n/* Micro-frontend Sections */\n.content-area section {\nposition: relative;\nflex: 1;\nbackground-color: var(--surface-color);\nborder-radius: 8px;\npadding: 20px;\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\ndisplay: none;\nmin-height: 400px;\n}\n.content-area section.active {\ndisplay: block;\n}\n/* Shell Footer */\n.shell-footer {\nposition: relative;\nheight: var(--footer-height);\nbackground-color: var(--surface-color);\nborder-radius: 8px;\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\ndisplay: flex;\nalign-items: center;\njustify-content: center;\ntext-align: center;\nmargin-bottom: 20px;\n}\n.footer-content {\ncolor: var(--text-secondary);\nfont-size: 14px;\n}\n/* Event Log */\n.event-log {\nbackground-color: var(--surface-color);\nborder-radius: 8px;\npadding: 20px;\nmargin-bottom: 20px;\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n.event-log h3 {\nmargin-bottom: 10px;\nfont-size: 16px;\ncolor: var(--text-color);\n}\n#event-log-content {\nheight: 150px;\noverflow-y: auto;\nbackground-color: #f8fafc;\nborder: 1px solid var(--border-color);\nborder-radius: 4px;\npadding: 10px;\nfont-family: monospace;\nfont-size: 14px;\n}\n.log-entry {\nmargin-bottom: 8px;\npadding-bottom: 8px;\nborder-bottom: 1px solid var(--border-color);\n}\n.log-entry:last-child {\nmargin-bottom: 0;\npadding-bottom: 0;\nborder-bottom: none;\n}\n.log-time {\nfont-size: 12px;\ncolor: var(--text-secondary);\n}\n.log-message {\nmargin-top: 2px;\n}\n/* Architecture Diagram */\n.architecture-diagram {\nbackground-color: var(--surface-color);\nborder-radius: 8px;\npadding: 20px;\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);\n}\n.architecture-diagram h3 {\nmargin-bottom: 20px;\nfont-size: 16px;\ncolor: var(--text-color);\n}\n.diagram {\ndisplay: flex;\njustify-content: center;\n}\n.diagram-shell {\nwidth: 80%;\nmax-width: 800px;\n}\n.diagram-label {\ntext-align: center;\nmargin-bottom: 10px;\nfont-weight: 500;\nfont-size: 14px;\ncolor: var(--core-team-color);\n}\n.diagram-container {\nborder: 2px solid var(--core-team-color);\nborder-radius: 8px;\npadding: 10px;\nbackground-color: var(--core-team-light);\n}\n.diagram-header,\n.diagram-footer {\nheight: 30px;\nbackground-color: rgba(59, 130, 246, 0.2);\nborder-radius: 4px;\nmargin-bottom: 10px;\n}\n.diagram-footer {\nmargin-bottom: 0;\nmargin-top: 10px;\n}\n.diagram-nav {\nwidth: 20%;\nheight: 150px;\nbackground-color: rgba(59, 130, 246, 0.2);\nborder-radius: 4px;\nfloat: left;\nmargin-right: 10px;\n}\n.diagram-content {\noverflow: hidden;\ndisplay: grid;\ngrid-template-columns: repeat(2, 1fr);\ngap: 10px;\nmin-height: 150px;\n}\n.diagram-mfe {\nborder-radius: 4px;\npadding: 15px;\nfont-size: 12px;\ntext-align: center;\nfont-weight: 500;\ncolor: white;\nposition: relative;\n}\n.diagram-mfe::after {\nposition: absolute;\nbottom: 5px;\nleft: 0;\nright: 0;\ntext-align: center;\ncolor: rgba(255, 255, 255, 0.8);\n}\n.diagram-mfe.accounts {\nbackground-color: var(--accounts-team-color);\n}\n.diagram-mfe.accounts::after {\ncontent: \"Accounts Team\";\n}\n.diagram-mfe.investments {\nbackground-color: var(--investments-team-color);\n}\n.diagram-mfe.investments::after {\ncontent: \"Investments Team\";\n}\n.diagram-mfe.loans {\nbackground-color: var(--loans-team-color);\n}\n.diagram-mfe.loans::after {\ncontent: \"Loans Team\";\n}\n.diagram-mfe.support {\nbackground-color: var(--support-team-color);\n}\n.diagram-mfe.support::after {\ncontent: \"Support Team\";\n}\n/* Responsive Adjustments */\n@media (max-width: 768px) {\n.content-area {\nflex-direction: column;\n}\n.shell-nav {\nwidth: 100%;\nmargin-right: 0;\nmargin-bottom: 20px;\n}\n.diagram-nav {\nwidth: 100%;\nheight: 40px;\nfloat: none;\nmargin-right: 0;\nmargin-bottom: 10px;\n}\n.diagram-content {\ngrid-template-columns: 1fr;\n}\n}\n/* MFE Component Styles */\naccounts-dashboard {\ndisplay: block;\npadding: 20px;\nborder: 2px solid var(--accounts-team-color);\nborder-radius: 8px;\nbackground-color: var(--accounts-team-light);\nmin-height: 300px;\n}\ninvestments-summary {\ndisplay: block;\npadding: 20px;\nborder: 2px solid var(--investments-team-color);\nborder-radius: 8px;\nbackground-color: var(--investments-team-light);\nmin-height: 300px;\n}\nloans-overview {\ndisplay: block;\npadding: 20px;\nborder: 2px solid var(--loans-team-color);\nborder-radius: 8px;\nbackground-color: var(--loans-team-light);\nmin-height: 300px;\n}\nsupport-widget {\ndisplay: block;\npadding: 20px;\nborder: 2px solid var(--support-team-color);\nborder-radius: 8px;\nbackground-color: var(--support-team-light);\nmin-height: 300px;\n}\nJavaScript\n// Log events to the event log\nfunction logEvent(source, action, data = null) {\nconst logContainer = document.getElementById('event-log-content');\nconst entry = document.createElement('div');\nentry.className = 'log-entry';\nconst time = document.createElement('div');\ntime.className = 'log-time';\ntime.textContent = new Date().toLocaleTimeString();\nconst message = document.createElement('div');\nmessage.className = 'log-message';\nmessage.textContent = `${source}: ${action}${data ? ` - ${JSON.stringify(data)}` : ''}`;\nentry.appendChild(time);\nentry.appendChild(message);\nlogContainer.appendChild(entry);\nlogContainer.scrollTop = logContainer.scrollHeight;\n}\n// Custom Event Bus for cross-team communication\nclass EventBus extends EventTarget {\nconstructor() {\nsuper();\n}\npublish(event, data) {\nthis.dispatchEvent(new CustomEvent(event, { detail: data }));\nlogEvent('EventBus', `Published \"${event}\"`, data);\n}\nsubscribe(event, callback) {\nthis.addEventListener(event, e => callback(e.detail));\nlogEvent('EventBus', `Subscribed to \"${event}\"`);\n}}\n// Create a shared event bus\nconst eventBus = new EventBus();\n// Mock User Data (in a real app, this would come from a backend)\nconst userData = {\naccounts: [\n{ id: 'a001', type: 'Checking', name: 'Primary Checking', balance: 3240.56 },\n{ id: 'a002', type: 'Savings', name: 'Emergency Fund', balance: 15750.25 }],\ninvestments: [\n{ id: 'i001', type: 'Stock', name: 'Tech Growth Fund', value: 8640.75, change: 12.3 },\n{ id: 'i002', type: 'Bond', name: 'Municipal Bond Fund', value: 12500.00, change: 3.2 }],\nloans: [\n{ id: 'l001', type: 'Mortgage', name: 'Home Loan', balance: 245000.00, rate: 3.5 },\n{ id: 'l002', type: 'Auto', name: 'Car Loan', balance: 12500.00, rate: 4.2 }] };\n// Create the Accounts Dashboard Component\nclass AccountsDashboard extends HTMLElement {\nconstructor() {\nsuper();\nthis.accounts = userData.accounts;\n}\nconnectedCallback() {\nlogEvent('Accounts MFE', 'Initialized');\nthis.render();\n// Listen for user selection events from other MFEs\neventBus.subscribe('user-selected', data => {\nlogEvent('Accounts MFE', 'Received user-selected event', data);\n// In a real app, we would update the displayed accounts\n});\n}\nrender() {\nthis.innerHTML = `\n<div class=\"accounts-dashboard\">\n<h2>Your Accounts</h2>\n<div class=\"accounts-list\">\n${this.accounts.map(account => `\n<div class=\"account-card\">\n<div class=\"account-type\">${account.type}</div>\n<div class=\"account-name\">${account.name}</div>\n<div class=\"account-balance\">$${account.balance.toFixed(2)}</div>\n<button class=\"view-details-btn\" data-id=\"${account.id}\">View Details</button>\n</div>\n`).join('')}\n</div>\n<div class=\"dashboard-actions\">\n<button class=\"primary-action\">Transfer Money</button>\n<button>Open New Account</button>\n</div>\n</div>\n<style>\n.accounts-dashboard h2 {\ncolor: var(--accounts-team-color);\nmargin-bottom: 20px;\n}\n.accounts-list {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\ngap: 16px;\nmargin-bottom: 20px;\n}\n.account-card {\nbackground-color: white;\nborder-radius: 8px;\npadding: 16px;\nbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n}\n.account-type {\nfont-size: 12px;\ncolor: var(--text-secondary);\nmargin-bottom: 4px;\n}\n.account-name {\nfont-weight: 600;\nmargin-bottom: 12px;\n}\n.account-balance {\nfont-size: 20px;\nfont-weight: 600;\ncolor: var(--accounts-team-color);\nmargin-bottom: 16px;\n}\n.view-details-btn {\ndisplay: block;\nwidth: 100%;\npadding: 8px;\nbackground-color: var(--accounts-team-light);\ncolor: var(--accounts-team-color);\nborder: 1px solid var(--accounts-team-color);\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\nfont-size: 14px;\ntransition: all 0.2s;\n}\n.view-details-btn:hover {\nbackground-color: var(--accounts-team-color);\ncolor: white;\n}\n.dashboard-actions {\ndisplay: flex;\ngap: 12px;\n}\n.dashboard-actions button {\npadding: 8px 16px;\nbackground-color: white;\nborder: 1px solid var(--border-color);\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\n}\n.primary-action {\nbackground-color: var(--accounts-team-color) !important;\ncolor: white;\nborder-color: var(--accounts-team-color) !important;\n}\n</style>\n`;\n// Add event listeners\nthis.querySelectorAll('.view-details-btn').forEach(btn => {\nbtn.addEventListener('click', event => {\nconst accountId = event.target.dataset.id;\nconst account = this.accounts.find(a => a.id === accountId);\nlogEvent('Accounts MFE', 'Account selected', { id: accountId });\neventBus.publish('account-selected', account);\n});\n});\n}}\n// Create the Investments Summary Component\nclass InvestmentsSummary extends HTMLElement {\nconstructor() {\nsuper();\nthis.investments = userData.investments;\n}\nconnectedCallback() {\nlogEvent('Investments MFE', 'Initialized');\nthis.render();\n// Listen for account selection events\neventBus.subscribe('account-selected', account => {\nlogEvent('Investments MFE', 'Received account-selected event', account);\n// In a real app, we might show investment recommendations based on the account\n});\n}\nrender() {\nthis.innerHTML = `\n<div class=\"investments-summary\">\n<h2>Your Investments</h2>\n<div class=\"portfolio-summary\">\n<div class=\"portfolio-value\">\n<div class=\"summary-label\">Total Portfolio Value</div>\n<div class=\"summary-value\">$${this.getTotalValue().toFixed(2)}</div>\n</div>\n<div class=\"portfolio-performance\">\n<div class=\"summary-label\">Overall Performance</div>\n<div class=\"summary-value ${this.getAverageChange() >= 0 ? 'positive' : 'negative'}\">\n${this.getAverageChange() >= 0 ? '+' : ''}${this.getAverageChange().toFixed(1)}%\n</div>\n</div>\n</div>\n<div class=\"investments-list\">\n${this.investments.map(inv => `\n<div class=\"investment-card\">\n<div class=\"investment-type\">${inv.type}</div>\n<div class=\"investment-name\">${inv.name}</div>\n<div class=\"investment-value\">$${inv.value.toFixed(2)}</div>\n<div class=\"investment-change ${inv.change >= 0 ? 'positive' : 'negative'}\">\n${inv.change >= 0 ? '+' : ''}${inv.change}%\n</div>\n<button class=\"view-details-btn\" data-id=\"${inv.id}\">View Details</button>\n</div>\n`).join('')}\n</div>\n</div>\n<style>\n.investments-summary h2 {\ncolor: var(--investments-team-color);\nmargin-bottom: 20px;\n}\n.portfolio-summary {\ndisplay: flex;\ngap: 20px;\nmargin-bottom: 24px;\n}\n.portfolio-value, .portfolio-performance {\nflex: 1;\nbackground-color: white;\npadding: 16px;\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n}\n.summary-label {\nfont-size: 14px;\ncolor: var(--text-secondary);\nmargin-bottom: 8px;\n}\n.summary-value {\nfont-size: 24px;\nfont-weight: 600;\n}\n.positive {\ncolor: var(--accounts-team-color);\n}\n.negative {\ncolor: var(--support-team-color);\n}\n.investments-list {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\ngap: 16px;\n}\n.investment-card {\nbackground-color: white;\nborder-radius: 8px;\npadding: 16px;\nbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n}\n.investment-type {\nfont-size: 12px;\ncolor: var(--text-secondary);\nmargin-bottom: 4px;\n}\n.investment-name {\nfont-weight: 600;\nmargin-bottom: 12px;\n}\n.investment-value {\nfont-size: 18px;\nfont-weight: 600;\nmargin-bottom: 4px;\n}\n.investment-change {\nmargin-bottom: 16px;\nfont-weight: 500;\n}\n.view-details-btn {\ndisplay: block;\nwidth: 100%;\npadding: 8px;\nbackground-color: var(--investments-team-light);\ncolor: var(--investments-team-color);\nborder: 1px solid var(--investments-team-color);\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\nfont-size: 14px;\ntransition: all 0.2s;\n}\n.view-details-btn:hover {\nbackground-color: var(--investments-team-color);\ncolor: white;\n}\n</style>\n`;\n// Add event listeners\nthis.querySelectorAll('.view-details-btn').forEach(btn => {\nbtn.addEventListener('click', event => {\nconst investmentId = event.target.dataset.id;\nconst investment = this.investments.find(i => i.id === investmentId);\nlogEvent('Investments MFE', 'Investment selected', { id: investmentId });\neventBus.publish('investment-selected', investment);\n});\n});\n}\ngetTotalValue() {\nreturn this.investments.reduce((sum, inv) => sum + inv.value, 0);\n}\ngetAverageChange() {\nreturn this.investments.reduce((sum, inv) => sum + inv.change, 0) / this.investments.length;\n}}\n// Create the Loans Overview Component\nclass LoansOverview extends HTMLElement {\nconstructor() {\nsuper();\nthis.loans = userData.loans;\n}\nconnectedCallback() {\nlogEvent('Loans MFE', 'Initialized');\nthis.render();\n// Listen for investment selection events\neventBus.subscribe('investment-selected', investment => {\nlogEvent('Loans MFE', 'Received investment-selected event', investment);\n// In a real app, we might show loan refinancing options based on investment performance\n});\n}\nrender() {\nthis.innerHTML = `\n<div class=\"loans-overview\">\n<h2>Your Loans</h2>\n<div class=\"loans-summary\">\n<div class=\"total-owed\">\n<div class=\"summary-label\">Total Balance</div>\n<div class=\"summary-value\">$${this.getTotalBalance().toFixed(2)}</div>\n</div>\n<div class=\"avg-rate\">\n<div class=\"summary-label\">Average Rate</div>\n<div class=\"summary-value\">${this.getAverageRate().toFixed(1)}%</div>\n</div>\n</div>\n<div class=\"loans-list\">\n${this.loans.map(loan => `\n<div class=\"loan-card\">\n<div class=\"loan-type\">${loan.type}</div>\n<div class=\"loan-name\">${loan.name}</div>\n<div class=\"loan-details\">\n<div>\n<span class=\"detail-label\">Balance:</span>\n<span class=\"detail-value\">$${loan.balance.toFixed(2)}</span>\n</div>\n<div>\n<span class=\"detail-label\">Rate:</span>\n<span class=\"detail-value\">${loan.rate}%</span>\n</div>\n</div>\n<button class=\"view-details-btn\" data-id=\"${loan.id}\">View Details</button>\n</div>\n`).join('')}\n</div>\n<div class=\"loans-actions\">\n<button class=\"primary-action\">Make a Payment</button>\n<button>Refinance Options</button>\n</div>\n</div>\n<style>\n.loans-overview h2 {\ncolor: var(--loans-team-color);\nmargin-bottom: 20px;\n}\n.loans-summary {\ndisplay: flex;\ngap: 20px;\nmargin-bottom: 24px;\n}\n.total-owed, .avg-rate {\nflex: 1;\nbackground-color: white;\npadding: 16px;\nborder-radius: 8px;\nbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n}\n.summary-label {\nfont-size: 14px;\ncolor: var(--text-secondary);\nmargin-bottom: 8px;\n}\n.summary-value {\nfont-size: 24px;\nfont-weight: 600;\n}\n.loans-list {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\ngap: 16px;\nmargin-bottom: 20px;\n}\n.loan-card {\nbackground-color: white;\nborder-radius: 8px;\npadding: 16px;\nbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n}\n.loan-type {\nfont-size: 12px;\ncolor: var(--text-secondary);\nmargin-bottom: 4px;\n}\n.loan-name {\nfont-weight: 600;\nmargin-bottom: 12px;\n}\n.loan-details {\nmargin-bottom: 16px;\n}\n.detail-label {\ncolor: var(--text-secondary);\nmargin-right: 4px;\n}\n.detail-value {\nfont-weight: 500;\n}\n.view-details-btn {\ndisplay: block;\nwidth: 100%;\npadding: 8px;\nbackground-color: var(--loans-team-light);\ncolor: var(--loans-team-color);\nborder: 1px solid var(--loans-team-color);\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\nfont-size: 14px;\ntransition: all 0.2s;\n}\n.view-details-btn:hover {\nbackground-color: var(--loans-team-color);\ncolor: white;\n}\n.loans-actions {\ndisplay: flex;\ngap: 12px;\n}\n.loans-actions button {\npadding: 8px 16px;\nbackground-color: white;\nborder: 1px solid var(--border-color);\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\n}\n.primary-action {\nbackground-color: var(--loans-team-color) !important;\ncolor: white;\nborder-color: var(--loans-team-color) !important;\n}\n</style>\n`;\n// Add event listeners\nthis.querySelectorAll('.view-details-btn').forEach(btn => {\nbtn.addEventListener('click', event => {\nconst loanId = event.target.dataset.id;\nconst loan = this.loans.find(l => l.id === loanId);\nlogEvent('Loans MFE', 'Loan selected', { id: loanId });\neventBus.publish('loan-selected', loan);\n});\n});\n}\ngetTotalBalance() {\nreturn this.loans.reduce((sum, loan) => sum + loan.balance, 0);\n}\ngetAverageRate() {\nreturn this.loans.reduce((sum, loan) => sum + loan.rate, 0) / this.loans.length;\n}}\n// Create the Support Widget Component\nclass SupportWidget extends HTMLElement {\nconstructor() {\nsuper();\n}\nconnectedCallback() {\nlogEvent('Support MFE', 'Initialized');\nthis.render();\n// Listen for various events to provide context-aware support\neventBus.subscribe('account-selected', account => {\nthis.updateSupportContext('account', account);\n});\neventBus.subscribe('investment-selected', investment => {\nthis.updateSupportContext('investment', investment);\n});\neventBus.subscribe('loan-selected', loan => {\nthis.updateSupportContext('loan', loan);\n});\n}\nupdateSupportContext(type, data) {\nlogEvent('Support MFE', `Context updated to ${type}`, data);\nconst contextDisplay = this.querySelector('.context-aware-support');\nif (contextDisplay) {\nlet contextText = '';\nswitch (type) {\ncase 'account':\ncontextText = `Need help with your ${data.name} account?`;\nbreak;\ncase 'investment':\ncontextText = `Questions about your ${data.name} investment?`;\nbreak;\ncase 'loan':\ncontextText = `Need assistance with your ${data.name}?`;\nbreak;}\ncontextDisplay.textContent = contextText;\ncontextDisplay.style.display = 'block';\n}\n}\nrender() {\nthis.innerHTML = `\n<div class=\"support-widget\">\n<h2>Customer Support</h2>\n<div class=\"context-aware-support\" style=\"display: none;\">\n<!-- Dynamically updated based on user interactions -->\n</div>\n<div class=\"support-options\">\n<div class=\"support-option\">\n<div class=\"option-icon\">📞</div>\n<div class=\"option-details\">\n<div class=\"option-title\">Call Us</div>\n<div class=\"option-description\">Speak with a representative</div>\n<div class=\"option-action\">1-800-555-0123</div>\n</div>\n</div>\n<div class=\"support-option\">\n<div class=\"option-icon\">💬</div>\n<div class=\"option-details\">\n<div class=\"option-title\">Live Chat</div>\n<div class=\"option-description\">Chat with a support agent</div>\n<button class=\"option-button\">Start Chat</button>\n</div>\n</div>\n<div class=\"support-option\">\n<div class=\"option-icon\">✉️</div>\n<div class=\"option-details\">\n<div class=\"option-title\">Email</div>\n<div class=\"option-description\">Send us a message</div>\n<button class=\"option-button\">Contact Form</button>\n</div>\n</div>\n</div>\n<div class=\"faq-section\">\n<h3>Frequently Asked Questions</h3>\n<ul class=\"faq-list\">\n<li><a href=\"#\" class=\"faq-link\">How do I reset my password?</a></li>\n<li><a href=\"#\" class=\"faq-link\">How to dispute a transaction?</a></li>\n<li><a href=\"#\" class=\"faq-link\">What are the wire transfer fees?</a></li>\n<li><a href=\"#\" class=\"faq-link\">How to order a replacement card?</a></li>\n</ul>\n</div>\n</div>\n<style>\n.support-widget h2 {\ncolor: var(--support-team-color);\nmargin-bottom: 20px;\n}\n.context-aware-support {\nbackground-color: var(--support-team-light);\nborder-left: 4px solid var(--support-team-color);\npadding: 12px 16px;\nmargin-bottom: 20px;\nborder-radius: 0 4px 4px 0;\nfont-weight: 500;\n}\n.support-options {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\ngap: 16px;\nmargin-bottom: 24px;\n}\n.support-option {\nbackground-color: white;\nborder-radius: 8px;\npadding: 16px;\nbox-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\ndisplay: flex;\ngap: 12px;\n}\n.option-icon {\nfont-size: 24px;\n}\n.option-details {\nflex: 1;\n}\n.option-title {\nfont-weight: 600;\nmargin-bottom: 4px;\n}\n.option-description {\ncolor: var(--text-secondary);\nfont-size: 14px;\nmargin-bottom: 12px;\n}\n.option-action {\nfont-weight: 500;\ncolor: var(--support-team-color);\n}\n.option-button {\npadding: 6px 12px;\nbackground-color: var(--support-team-color);\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-family: inherit;\nfont-size: 14px;\n}\n.faq-section h3 {\nmargin-bottom: 12px;\nfont-size: 16px;\n}\n.faq-list {\nlist-style: none;\npadding: 0;\n}\n.faq-list li {\nmargin-bottom: 8px;\n}\n.faq-link {\ncolor: var(--support-team-color);\ntext-decoration: none;\n}\n.faq-link:hover {\ntext-decoration: underline;\n}\n</style>\n`;\n// Add event listeners\nthis.querySelectorAll('.option-button').forEach(btn => {\nbtn.addEventListener('click', event => {\nconst action = event.target.textContent;\nlogEvent('Support MFE', `Support action initiated: ${action}`);\n});\n});\nthis.querySelectorAll('.faq-link').forEach(link => {\nlink.addEventListener('click', event => {\nevent.preventDefault();\nconst question = event.target.textContent;\nlogEvent('Support MFE', `FAQ clicked: ${question}`);\n});\n});\n}}\n// Register the custom elements\ncustomElements.define('accounts-dashboard', AccountsDashboard);\ncustomElements.define('investments-summary', InvestmentsSummary);\ncustomElements.define('loans-overview', LoansOverview);\ncustomElements.define('support-widget', SupportWidget);\n// Set up navigation\ndocument.addEventListener('DOMContentLoaded', () => {\nconst navItems = document.querySelectorAll('.nav-item');\nnavItems.forEach(item => {\nitem.addEventListener('click', () => {\n// Update active nav item\nnavItems.forEach(nav => nav.classList.remove('active'));\nitem.classList.add('active');\n// Show corresponding section\nconst section = item.dataset.section;\ndocument.querySelectorAll('.content-area section').forEach(s => {\ns.classList.remove('active');\n});\ndocument.getElementById(`microfrontend-${section}`).classList.add('active');\nlogEvent('Shell Application', `Navigated to ${section} section`);\n});\n});\n// Log initialization\nlogEvent('Shell Application', 'Micro-Frontend demo initialized');\n});\nMicro-Frontend Web Component Demo on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/).\nThis simplified banking portal demonstrates the core principles of micro-frontend architecture:\nTeam Ownership Separation - Each colored section is owned by a different team\nIndependent Development - Each team can build and deploy their component independently\nCross-Team Communication - Components interact through a centralized event bus\nConsistent User Experience - Despite being developed separately, components provide a unified experience\nTry interacting with different components and watch the event log to see how they communicate. When you click on an account in the Accounts section, notice how the Support widget responds with contextual information - demonstrating cross-component communication.\nConclusion\nYou can now do four concrete things with a web component:\nCompose it from slots so the page decides the content and the component decides the layout.\nPick the right input: attributes for small string settings, properties for arrays and objects.\nSend events out with bubbles: true and composed: true so listeners outside the shadow root hear them.\nKeep names predictable (variant, size, disabled) so one component teaches you the next.\nThe sections on lazy-loading, constructable stylesheets, accessibility, framework wrappers and micro-frontends are worked examples of those four habits under pressure; the post on frameworks (https://mnixblog.netlify.app/advanced-patterns-and-integration-with-frameworks/) takes the framework wrappers further. The last post in the series is a dated status board of what ships and what does not.\nResources\nWeb Components on MDN (https://developer.mozilla.org/en-US/docs/Web/Web_Components)\nGoogle's Lit library (https://lit.dev/)\nMicrosoft's FAST components (https://www.fast.design/)\nAdobe's Spectrum Web Components (https://opensource.adobe.com/spectrum-web-components/)\nShoelace components (https://shoelace.style/)\nWebComponents.org (https://www.webcomponents.org/)\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-04-06T04:00:20Z",
      "id": "https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/",
      "image": "https://mnixblog.netlify.app/images/real-world-examples-and-patterns-in-web-components/cover-v4.jpg",
      "summary": "See who publishes web components (Google, Microsoft, Adobe and others) and four patterns that make them easy to use, with runnable examples.",
      "tags": [
        "JavaScript",
        "Web Components",
        "HTML",
        "Beginners"
      ],
      "title": "Real-World Examples & Patterns in Web Components",
      "url": "https://mnixblog.netlify.app/real-world-examples-and-patterns-in-web-components/"
    },
    {
      "content_html": "<h2 id=\"introduction\">Introduction<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#introduction\" aria-label=\"Link to this section\">#</a></h2>\n<p>Meet Sarah, a web user who navigates the internet using a screen reader. When she encounters custom elements on websites, her experience isn't determined by visual design—it's determined by whether developers prioritized accessibility. When developers overlook the needs of millions of users with disabilities, they transform bridges into barriers, creating frustration instead of inclusive solutions.</p>\n<p>A screen reader reads a page aloud from its structure, not from how it looks. A custom element that only looks like a button is silent or confusing to Sarah, and often unreachable for anyone who cannot use a mouse. This post builds custom elements that say what they are, work from the keyboard, and announce changes.</p>\n<p><strong>Quick recap if you are joining here:</strong> a custom element is a class that <code>extends HTMLElement</code>, registered with <code>customElements.define('my-tag', MyClass)</code>, and Shadow DOM gives it a private inside that the page's CSS and scripts cannot reach by accident. <strong>ARIA</strong> (Accessible Rich Internet Applications) is a set of HTML attributes, such as <code>role</code>, <code>aria-pressed</code> and <code>aria-label</code>, that tell assistive technology what a control is and what state it is in. ARIA only changes what is announced; it adds no behavior. Whenever a native element does the job (a real <code>&lt;button&gt;</code>, for instance), use it and skip the ARIA.</p>\n<p>A reusable element is only reusable if everyone can use it. This guide covers semantic HTML, ARIA roles, keyboard navigation and testing for custom elements, with interactive examples you can change and run.</p>\n<p>With these fundamentals in mind, let's see accessibility in action.</p>\n<h2 id=\"the-accessibility-difference\">The Accessibility Difference<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#the-accessibility-difference\" aria-label=\"Link to this section\">#</a></h2>\n<p>The following examples demonstrate how seemingly small details create dramatically different user experiences:</p>\n<p>Before diving into details, let's visualize the difference accessibility makes:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Accessiblity in Web Components Example 1</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h3&gt;Non-accessible Toggle&lt;/h3&gt;\n&lt;div class=&quot;toggle-button&quot; onclick=&quot;this.classList.toggle(&#39;active&#39;)&quot;&gt;\n\t&lt;span class=&quot;toggle-text&quot;&gt;&lt;/span&gt;\n\n&lt;/div&gt;\n\n&lt;h3&gt;Accessible Toggle&lt;/h3&gt;\n&lt;accessible-toggle&gt;&lt;/accessible-toggle&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Simple Dark Mode CSS */\n\n/* Default light mode */\n:root {\n\t--bg-color: #ffffff;\n\t--text-color: #333333;\n}\n\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t}\n}\n\n/* Basic styling */\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tsans-serif;\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 0;\n\ttransition: background-color 0.3s ease, color 0.3s ease;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 100vh;\n\ttext-align: center;\n}\n\n.toggle-button {\n\tpadding: 8px 16px;\n\tborder: 2px solid #333;\n\tborder-radius: 4px;\n\tfont-size: 16px;\n\tcursor: pointer;\n\ttransition: all 0.3s;\n\tbackground: #ccc;\n\tcolor: #000;\n}\n\n.toggle-button.active {\n\tbackground-color: #4caf50;\n\tcolor: white;\n}\n\n.toggle-button.active .toggle-text {\n\tcontent: &quot;ON&quot;;\n}\n\n.active .toggle-text:after {\n\tcontent: &quot;ON&quot;;\n}\n\n.toggle-text:after {\n\tcontent: &quot;OFF&quot;;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class AccessibleToggle extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.pressed = false;\n    this.render();\n    // A native &lt;button&gt; already handles Enter and Space, so one click listener is enough.\n    this.shadowRoot.querySelector(&#39;button&#39;).addEventListener(&#39;click&#39;, () =&gt; this.toggle());\n  }\n\n  toggle() {\n    this.pressed = !this.pressed;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;toggle&#39;, {\n      bubbles: true,\n      detail: { pressed: this.pressed } }));\n\n  }\n\n  render() {\n    const button = this.shadowRoot.querySelector(&#39;button&#39;) || document.createElement(&#39;button&#39;);\n    button.setAttribute(&#39;aria-pressed&#39;, this.pressed);\n    const label = this.getAttribute(&#39;label&#39;) || &#39;Toggle&#39;;\n    // The name comes from the visible text; aria-pressed carries the state.\n    button.innerHTML = `${label} &lt;span aria-hidden=&quot;true&quot;&gt;${this.pressed ? &#39;ON&#39; : &#39;OFF&#39;}&lt;/span&gt;`;\n    button.className = this.pressed ? &#39;pressed&#39; : &#39;&#39;;\n\n    if (!this.shadowRoot.querySelector(&#39;button&#39;)) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          button {\n            padding: 8px 16px;\n            border: 2px solid #333;\n            border-radius: 4px;\n            font-size: 16px;\n            cursor: pointer;\n            transition: all 0.3s;\n          }\n          button:focus-visible {\n            outline: 3px solid blue;\n            outline-offset: 2px;\n          }\n          button.pressed {\n            background-color: #4CAF50;\n            color: white;\n          }\n          button:not(.pressed) {\n            background-color: #f1f1f1;\n          }\n        &lt;/style&gt;\n      `;\n      this.shadowRoot.appendChild(button);\n    }\n  }}\n\n\ncustomElements.define(&#39;accessible-toggle&#39;, AccessibleToggle);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<p>Try using only your keyboard (Tab and Enter) to interact with both buttons above. Notice how the accessible version provides:</p>\n<ul>\n<li>Clear focus indication</li>\n<li>Proper state announcement</li>\n<li>Keyboard operability</li>\n</ul>\n<p>The first toggle is a plain <code>&lt;div&gt;</code> with a click handler: it has no role, no keyboard stop, and nothing that says \"pressed\". For Sarah, that is a rectangle she cannot find and cannot operate. These seemingly small details make a tremendous difference for users relying on assistive technologies.</p>\n<h2 id=\"understanding-accessibility-in-web-components\">Understanding Accessibility in Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#understanding-accessibility-in-web-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>Now that we've seen the tangible impact of accessibility features, let's explore the core principles that make web components truly inclusive for all users.</p>\n<p>Accessibility (often abbreviated as A11y) involves designing products usable by people with disabilities. For Web Components, this means ensuring custom elements are perceivable, operable, understandable, and robust for all users.</p>\n<p><strong>Key Considerations:</strong></p>\n<ul>\n<li><strong>Semantic HTML:</strong> Use appropriate HTML elements to convey meaning, as browsers and assistive technologies rely on these semantics.</li>\n<li><strong>ARIA Roles and Properties:</strong> When native HTML elements are insufficient, ARIA roles and properties provide additional accessibility information.</li>\n<li><strong>Keyboard Accessibility:</strong> Ensure all interactive elements are navigable and operable via keyboard.</li>\n<li><strong>Focus Management:</strong> Properly manage focus to assist users navigating with keyboards or assistive devices.</li>\n</ul>\n<p>While these accessibility considerations apply to all web development, Web Components introduce unique challenges through their use of Shadow DOM—an essential feature that requires thoughtful implementation.</p>\n<h3 id=\"the-shadow-dom-challenge\">The Shadow DOM Challenge<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#the-shadow-dom-challenge\" aria-label=\"Link to this section\">#</a></h3>\n<p>Web Components use Shadow DOM to encapsulate styles and functionality, creating unique accessibility challenges:</p>\n<ol>\n<li><strong>Style Isolation:</strong> Focus styles might not be visible if they rely on global CSS</li>\n<li><strong>DOM Isolation:</strong> ARIA relationships across shadow boundaries might not work as expected</li>\n<li><strong>Event Handling:</strong> Custom events must properly handle keyboard interactions</li>\n</ol>\n<p>Despite these challenges, you can create fully accessible Web Components with the right approach. Let's explore practical implementation strategies that address these concerns while maintaining the benefits of encapsulation.</p>\n<h2 id=\"implementing-accessibility-in-web-components\">Implementing Accessibility in Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#implementing-accessibility-in-web-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>These challenges can be overcome with proper implementation, as demonstrated in the examples below.</p>\n<h3 id=\"1-semantic-html-and-aria-roles\">1. Semantic HTML and ARIA Roles<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#1-semantic-html-and-aria-roles\" aria-label=\"Link to this section\">#</a></h3>\n<p>Start with native HTML elements that inherently provide accessibility. When creating custom elements, assign appropriate ARIA roles and properties to convey their purpose and state.</p>\n<h4 id=\"interactive-example-accessible-toggle-button\">Interactive Example: Accessible Toggle Button<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#interactive-example-accessible-toggle-button\" aria-label=\"Link to this section\">#</a></h4>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Accessible Toggle Switch Web Component</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;accessible-toggle&gt;&lt;/accessible-toggle&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Simple Dark Mode CSS */\n\n/* Default light mode */\n:root {\n\t--bg-color: #ffffff;\n\t--text-color: #333333;\n}\n\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t}\n}\n\n/* Basic styling */\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tsans-serif;\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 0;\n\ttransition: background-color 0.3s ease, color 0.3s ease;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 100vh;\n\ttext-align: center;\n}\n\n.toggle-button {\n\tpadding: 8px 16px;\n\tborder: 2px solid #333;\n\tborder-radius: 4px;\n\tfont-size: 16px;\n\tcursor: pointer;\n\ttransition: all 0.3s;\n\tbackground: #ccc;\n\tcolor: #000;\n}\n\n.toggle-button.active {\n\tbackground-color: #4caf50;\n\tcolor: white;\n}\n\n.toggle-button.active .toggle-text {\n\tcontent: &quot;ON&quot;;\n}\n\n.active .toggle-text:after {\n\tcontent: &quot;ON&quot;;\n}\n\n.toggle-text:after {\n\tcontent: &quot;OFF&quot;;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class AccessibleToggle extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.pressed = false;\n    this.render();\n    // A native &lt;button&gt; already handles Enter and Space, so one click listener is enough.\n    this.shadowRoot.querySelector(&#39;button&#39;).addEventListener(&#39;click&#39;, () =&gt; this.toggle());\n  }\n\n  toggle() {\n    this.pressed = !this.pressed;\n    this.render();\n    this.dispatchEvent(new CustomEvent(&#39;toggle&#39;, {\n      bubbles: true,\n      detail: { pressed: this.pressed } }));\n\n  }\n\n  render() {\n    const button = this.shadowRoot.querySelector(&#39;button&#39;) || document.createElement(&#39;button&#39;);\n    button.setAttribute(&#39;aria-pressed&#39;, this.pressed);\n    const label = this.getAttribute(&#39;label&#39;) || &#39;Toggle&#39;;\n    // The name comes from the visible text; aria-pressed carries the state.\n    button.innerHTML = `${label} &lt;span aria-hidden=&quot;true&quot;&gt;${this.pressed ? &#39;ON&#39; : &#39;OFF&#39;}&lt;/span&gt;`;\n    button.className = this.pressed ? &#39;pressed&#39; : &#39;&#39;;\n\n    if (!this.shadowRoot.querySelector(&#39;button&#39;)) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          button {\n            padding: 8px 16px;\n            border: 2px solid #333;\n            border-radius: 4px;\n            font-size: 16px;\n            cursor: pointer;\n            transition: all 0.3s;\n          }\n          button:focus-visible {\n            outline: 3px solid blue;\n            outline-offset: 2px;\n          }\n          button.pressed {\n            background-color: #4CAF50;\n            color: white;\n          }\n          button:not(.pressed) {\n            background-color: #f1f1f1;\n          }\n        &lt;/style&gt;\n      `;\n      this.shadowRoot.appendChild(button);\n    }\n  }}\n\n\ncustomElements.define(&#39;accessible-toggle&#39;, AccessibleToggle);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<pre><code class=\"language-javascript\">class AccessibleToggle extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.pressed = false;\n    this.render();\n    // A native &lt;button&gt; already handles Enter and Space, so one click listener is enough.\n    this.shadowRoot.querySelector('button').addEventListener('click', () =&gt; this.toggle());\n  }\n\n  toggle() {\n    this.pressed = !this.pressed;\n    this.render();\n    this.dispatchEvent(new CustomEvent('toggle', { \n      bubbles: true,\n      detail: { pressed: this.pressed }\n    }));\n  }\n\n  render() {\n    const button = this.shadowRoot.querySelector('button') || document.createElement('button');\n    button.setAttribute('aria-pressed', this.pressed);\n    const label = this.getAttribute('label') || 'Toggle';\n    // The name comes from the visible text; aria-pressed carries the state.\n    button.innerHTML = `${label} &lt;span aria-hidden=\"true\"&gt;${this.pressed ? 'ON' : 'OFF'}&lt;/span&gt;`;\n    button.className = this.pressed ? 'pressed' : '';\n\n    if (!this.shadowRoot.querySelector('button')) {\n      this.shadowRoot.innerHTML = `\n        &lt;style&gt;\n          button {\n            padding: 8px 16px;\n            border: 2px solid #333;\n            border-radius: 4px;\n            font-size: 16px;\n            cursor: pointer;\n            transition: all 0.3s;\n          }\n          button:focus-visible {\n            outline: 3px solid blue;\n            outline-offset: 2px;\n          }\n          button.pressed {\n            background-color: #4CAF50;\n            color: white;\n          }\n          button:not(.pressed) {\n            background-color: #f1f1f1;\n          }\n        &lt;/style&gt;\n      `;\n      this.shadowRoot.appendChild(button);\n    }\n  }\n}\n\ncustomElements.define('accessible-toggle', AccessibleToggle);\n</code></pre>\n<p><strong>Key accessibility features:</strong></p>\n<ul>\n<li>Uses a real <code>&lt;button&gt;</code>, so it is reachable with Tab and works with Enter and Space with no extra code</li>\n<li>Uses <code>aria-pressed</code> to convey the on/off state to assistive technologies</li>\n<li>Lets the visible text (\"Toggle\") be the accessible name, with the <code>ON</code>/<code>OFF</code> text hidden from screen readers because <code>aria-pressed</code> already says it</li>\n<li>Provides visible state indication through color and text changes</li>\n<li>Implements strong focus indication with high contrast outline</li>\n<li>Dispatches events that can be caught by parent components</li>\n</ul>\n<p>Try navigating to the button with Tab and activating it with Enter or Space. A screen reader typically announces something like \"Toggle, toggle button, not pressed\" and, after activation, \"Toggle, toggle button, pressed\". The exact wording varies between screen readers.</p>\n<p><strong>A mistake worth seeing.</strong> An earlier version of this toggle set <code>role=\"button\"</code> and <code>aria-label=\"Toggle\"</code> on the <code>&lt;button&gt;</code>. Both were harmful in small ways. <code>role=\"button\"</code> on a <code>&lt;button&gt;</code> repeats what the browser already knows. And <code>aria-label</code> replaces the visible text as the button's name, so the screen reader never says \"ON\" or \"OFF\", and a voice-control user who says \"click ON\" may find nothing with that name. In Chrome's accessibility tree the old version showed the name <code>Toggle</code> with the visible text <code>ON</code> sitting under it; the new one has the name <code>Toggle</code> and the state <code>pressed</code>, and the visible text agrees with what is announced. The rule behind it: do not override a visible label with a different <code>aria-label</code>.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A custom toggle is built from a native <code>&lt;button&gt;</code>. Which attribute should carry its on/off state for assistive technology?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>role=\"button\"</code></li>\n<li><code>tabindex=\"0\"</code></li>\n<li><code>aria-pressed</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. <code>aria-pressed</code></p>\n<p><code>aria-pressed=\"true\"</code> or <code>\"false\"</code> tells assistive technology the button is a toggle and which way it is set. <code>role=\"button\"</code> is already implied by the element, and <code>tabindex=\"0\"</code> only affects Tab order, which a native button already has.</p>\n</div>\n</div>\n</section>\n<h3 id=\"2-keyboard-accessibility-and-focus-management\">2. Keyboard Accessibility and Focus Management<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#2-keyboard-accessibility-and-focus-management\" aria-label=\"Link to this section\">#</a></h3>\n<p>With the foundation of proper semantics established, let's focus on how users navigate through interactive components without a mouse.</p>\n<p>Proper focus management matters, especially with dynamic content or modal dialogs. Users navigating with keyboards need clear focus indication and logical tab order.</p>\n<p>Two terms before the next example. <strong>Focus</strong> is the element that currently receives keystrokes; Tab moves it to the next interactive element. A <strong>focus trap</strong> is what a modal dialog does while it is open: Tab and Shift+Tab cycle among the controls inside the dialog instead of wandering to the page hidden behind it. When the dialog closes, focus goes back to the button that opened it.</p>\n<p>A note from October 2026: a native <code>&lt;dialog&gt;</code> opened with <code>showModal()</code> already traps focus, closes on Escape and returns focus to the opener, so for a plain modal you may not need a class like the one below. We build it by hand here to see what the browser does on our behalf.</p>\n<h4 id=\"interactive-example-accessible-modal-dialog\">Interactive Example: Accessible Modal Dialog<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#interactive-example-accessible-modal-dialog\" aria-label=\"Link to this section\">#</a></h4>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Accessible Modal Web Component</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;accessible-modal&gt;&lt;/accessible-modal&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Simple Dark Mode CSS */\n\n/* Default light mode */\n:root {\n\t--bg-color: #ffffff;\n\t--text-color: #333333;\n}\n\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t}\n}\n\n/* Basic styling */\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tsans-serif;\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 0;\n\ttransition: background-color 0.3s ease, color 0.3s ease;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 100vh;\n\ttext-align: center;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class AccessibleModal extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this._previouslyFocusedElement = null;\n    this._keydownHandler = null;\n    this._escapeHandler = null;\n    this.render();\n  }\n\n  connectedCallback() {\n    const trigger = this.shadowRoot.querySelector(&quot;#modal-trigger&quot;);\n    const closeBtn = this.shadowRoot.querySelector(&quot;#modal-close&quot;);\n    const dialog = this.shadowRoot.querySelector(&#39;[role=&quot;dialog&quot;]&#39;);\n    const overlay = this.shadowRoot.querySelector(&quot;.modal-overlay&quot;);\n\n    // Open modal on trigger click\n    trigger.addEventListener(&quot;click&quot;, () =&gt; this.open());\n\n    // Close modal on close button click\n    closeBtn.addEventListener(&quot;click&quot;, () =&gt; this.close());\n\n    // Create handler for Escape key\n    this._escapeHandler = e =&gt; {\n      if (e.key === &quot;Escape&quot; &amp;&amp; dialog.getAttribute(&quot;aria-hidden&quot;) !== &quot;true&quot;) {\n        this.close();\n      }\n    };\n\n    // Add Escape key event listener to the document\n    document.addEventListener(&quot;keydown&quot;, this._escapeHandler);\n\n    // Close when clicking outside the modal\n    overlay.addEventListener(&quot;click&quot;, e =&gt; {\n      if (e.target === overlay) {\n        this.close();\n      }\n    });\n  }\n\n  disconnectedCallback() {\n    // Clean up event listeners when component is removed\n    if (this._escapeHandler) {\n      document.removeEventListener(&quot;keydown&quot;, this._escapeHandler);\n    }\n    if (this._keydownHandler) {\n      document.removeEventListener(&quot;keydown&quot;, this._keydownHandler);\n    }\n  }\n\n  open() {\n    // Store currently focused element to restore later\n    this._previouslyFocusedElement = document.activeElement;\n    const dialog = this.shadowRoot.querySelector(&#39;[role=&quot;dialog&quot;]&#39;);\n    const overlay = this.shadowRoot.querySelector(&quot;.modal-overlay&quot;);\n\n    // Make modal visible\n    dialog.removeAttribute(&quot;aria-hidden&quot;);\n    overlay.style.display = &quot;flex&quot;;\n\n    // Focus the first focusable element (close button)\n    setTimeout(() =&gt; {\n      this.shadowRoot.querySelector(&quot;#modal-close&quot;).focus();\n    }, 50);\n\n    // Set up focus trapping\n    this._setupFocusTrap();\n\n    // Prevent background scrolling\n    document.body.style.overflow = &quot;hidden&quot;;\n  }\n\n  close() {\n    const dialog = this.shadowRoot.querySelector(&#39;[role=&quot;dialog&quot;]&#39;);\n    const overlay = this.shadowRoot.querySelector(&quot;.modal-overlay&quot;);\n\n    // Hide modal\n    dialog.setAttribute(&quot;aria-hidden&quot;, &quot;true&quot;);\n    overlay.style.display = &quot;none&quot;;\n\n    // Remove focus trap\n    if (this._keydownHandler) {\n      document.removeEventListener(&quot;keydown&quot;, this._keydownHandler);\n      this._keydownHandler = null;\n    }\n\n    // Re-enable scrolling\n    document.body.style.overflow = &quot;&quot;;\n\n    // Restore focus with a slight delay to ensure UI updates first\n    if (this._previouslyFocusedElement) {\n      console.log(&quot;Restoring focus to:&quot;, this._previouslyFocusedElement);\n      setTimeout(() =&gt; {\n        try {\n          this.shadowRoot.querySelector(&quot;#modal-trigger&quot;).focus();\n        } catch (e) {\n          console.error(&quot;Error restoring focus:&quot;, e);\n          // Fallback - focus the trigger button\n          this.shadowRoot.querySelector(&quot;#modal-trigger&quot;).focus();\n        }\n      }, 100);\n    } else {\n      // Fallback if no previous element was stored\n      setTimeout(() =&gt; {\n        this.shadowRoot.querySelector(&quot;#modal-trigger&quot;).focus();\n      }, 100);\n    }\n  }\n\n  _setupFocusTrap() {\n    // Get all focusable elements within the modal\n    const focusableElements = Array.from(\n    this.shadowRoot.querySelectorAll(\n    &#39;button, [href], input, select, textarea, [tabindex]:not([tabindex=&quot;-1&quot;])&#39;));\n\n\n\n    const firstFocusable = focusableElements[0];\n    const lastFocusable = focusableElements[focusableElements.length - 1];\n\n    // Remove any previous event listener\n    if (this._keydownHandler) {\n      document.removeEventListener(&quot;keydown&quot;, this._keydownHandler);\n    }\n\n    // Create and add the keydown handler for Tab key focus trapping\n    this._keydownHandler = e =&gt; {\n      // Only respond to Tab key\n      if (e.key === &quot;Tab&quot;) {\n        // If pressing Shift+Tab on the first focusable element, wrap to the last\n        if (e.shiftKey &amp;&amp; document.activeElement === firstFocusable) {\n          e.preventDefault();\n          lastFocusable.focus();\n        }\n        // If pressing Tab on the last focusable element, wrap to the first\n        else if (!e.shiftKey &amp;&amp; document.activeElement === lastFocusable) {\n            e.preventDefault();\n            firstFocusable.focus();\n          }\n      }\n    };\n\n    // Add the event listener to the document (more reliable than shadowRoot)\n    document.addEventListener(&quot;keydown&quot;, this._keydownHandler);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        /* Dark mode variables */\n        :host {\n          --bg-color: #ffffff;\n          --text-color: #333333;\n          --overlay-bg: rgba(0, 0, 0, 0.5);\n          --modal-bg: #ffffff;\n          --modal-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n          --button-border: #ccc;\n          --button-bg: #f5f5f5;\n          --button-hover: #e0e0e0;\n          --button-focus: blue;\n          --primary-button-bg: #4CAF50;\n          --primary-button-hover: #3d8b40;\n          --primary-button-color: white;\n          \n          /* Center content */\n          display: flex;\n          flex-direction: column;\n          align-items: center;\n          justify-content: center;\n          text-align: center;\n        }\n        \n        /* Dark mode detection */\n        @media (prefers-color-scheme: dark) {\n          :host {\n            --bg-color: #121212;\n            --text-color: #e0e0e0;\n            --overlay-bg: rgba(0, 0, 0, 0.7);\n            --modal-bg: #1e1e1e;\n            --modal-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n            --button-border: #444;\n            --button-bg: #2d2d2d;\n            --button-hover: #3d3d3d;\n            --button-focus: #7289da;\n            --primary-button-bg: #2e7d32;\n            --primary-button-hover: #266d29;\n            --primary-button-color: #e0e0e0;\n          }\n        }\n\n        .modal-overlay {\n          position: fixed;\n          top: 0;\n          left: 0;\n          right: 0;\n          bottom: 0;\n          background-color: var(--overlay-bg);\n          display: none;\n          justify-content: center;\n          align-items: center;\n          z-index: 1000;\n        }\n        \n        .modal-content {\n          background-color: var(--modal-bg);\n          color: var(--text-color);\n          padding: 20px;\n          border-radius: 4px;\n          max-width: 500px;\n          width: 90%;\n          box-shadow: var(--modal-shadow);\n          transition: background-color 0.3s ease, color 0.3s ease;\n        }\n        \n        button {\n          padding: 8px 16px;\n          cursor: pointer;\n          border-radius: 4px;\n          background-color: var(--button-bg);\n          color: var(--text-color);\n          border: 1px solid var(--button-border);\n          transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;\n        }\n        \n        button:hover {\n          background-color: var(--button-hover);\n        }\n        \n        button:focus-visible {\n          outline: 3px solid var(--button-focus);\n          outline-offset: 2px;\n        }\n        \n        #modal-close {\n          float: right;\n          background: none;\n          border: 1px solid var(--button-border);\n          font-size: 16px;\n        }\n        \n        #modal-trigger {\n          background-color: var(--primary-button-bg);\n          color: var(--primary-button-color);\n          border: none;\n          font-size: 16px;\n          margin: 20px 0;\n        }\n        \n        #modal-trigger:hover {\n          opacity: 0.9;\n          background-color: var(--primary-button-hover);\n        }\n        \n        .button-container {\n          display: flex;\n          justify-content: center;\n          gap: 10px;\n          margin-top: 15px;\n        }\n      &lt;/style&gt;\n      \n      &lt;button id=&quot;modal-trigger&quot;&gt;Open Modal&lt;/button&gt;\n      \n      &lt;div class=&quot;modal-overlay&quot;&gt;\n        &lt;div role=&quot;dialog&quot; aria-labelledby=&quot;modal-title&quot; aria-modal=&quot;true&quot; aria-hidden=&quot;true&quot; class=&quot;modal-content&quot;&gt;\n          &lt;button id=&quot;modal-close&quot; aria-label=&quot;Close dialog&quot;&gt;×&lt;/button&gt;\n          &lt;h2 id=&quot;modal-title&quot;&gt;Accessible Modal Dialog&lt;/h2&gt;\n          &lt;p&gt;This is an example of an accessible modal dialog component.&lt;/p&gt;\n          &lt;p&gt;It traps focus inside the modal, can be closed with the ESC key, and restores focus when closed.&lt;/p&gt;\n          &lt;div class=&quot;button-container&quot;&gt;\n            &lt;button&gt;Test Button 1&lt;/button&gt;\n            &lt;button&gt;Test Button 2&lt;/button&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\ncustomElements.define(&quot;accessible-modal&quot;, AccessibleModal);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<details>\n<summary>Accessible modal JavaScript · 138 lines</summary>\n<pre><code class=\"language-javascript\">class AccessibleModal extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this._previouslyFocusedElement = null;\n    this.render();\n  }\n\n  connectedCallback() {\n    const trigger = this.shadowRoot.querySelector('#modal-trigger');\n    const closeBtn = this.shadowRoot.querySelector('#modal-close');\n    const dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\n\n    trigger.addEventListener('click', () =&gt; this.open());\n    closeBtn.addEventListener('click', () =&gt; this.close());\n\n    // Close on Escape key\n    this.addEventListener('keydown', e =&gt; {\n      if (e.key === 'Escape' &amp;&amp; !dialog.hasAttribute('aria-hidden')) {\n        this.close();\n      }\n    });\n\n    // Close when clicking outside the modal\n    this.shadowRoot.querySelector('.modal-overlay').addEventListener('click', e =&gt; {\n      if (e.target.classList.contains('modal-overlay')) {\n        this.close();\n      }\n    });\n  }\n\n  open() {\n    this._previouslyFocusedElement = document.activeElement;\n    const dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\n    dialog.removeAttribute('aria-hidden');\n    this.shadowRoot.querySelector('.modal-overlay').style.display = 'flex';\n    this.shadowRoot.querySelector('#modal-close').focus();\n\n    // Trap focus inside modal\n    this._handleFocusTrap();\n  }\n\n  close() {\n    this.shadowRoot.querySelector('[role=\"dialog\"]').setAttribute('aria-hidden', 'true');\n    this.shadowRoot.querySelector('.modal-overlay').style.display = 'none';\n\n    // Restore focus\n    if (this._previouslyFocusedElement) {\n      this._previouslyFocusedElement.focus();\n    }\n  }\n\n  _handleFocusTrap() {\n    const focusableElements = this.shadowRoot.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');\n    const firstFocusable = focusableElements[0];\n    const lastFocusable = focusableElements[focusableElements.length - 1];\n\n    this.shadowRoot.addEventListener('keydown', e =&gt; {\n      if (e.key === 'Tab') {\n        if (e.shiftKey &amp;&amp; document.activeElement === firstFocusable) {\n          e.preventDefault();\n          lastFocusable.focus();\n        } else if (!e.shiftKey &amp;&amp; document.activeElement === lastFocusable) {\n          e.preventDefault();\n          firstFocusable.focus();\n        }\n      }\n    });\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .modal-overlay {\n          position: fixed;\n          top: 0;\n          left: 0;\n          right: 0;\n          bottom: 0;\n          background-color: rgba(0, 0, 0, 0.5);\n          display: none;\n          justify-content: center;\n          align-items: center;\n          z-index: 1000;\n        }\n\n        .modal-content {\n          background-color: white;\n          padding: 20px;\n          border-radius: 4px;\n          max-width: 500px;\n          width: 90%;\n          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n        }\n\n        button {\n          padding: 8px 16px;\n          cursor: pointer;\n          border-radius: 4px;\n        }\n\n        button:focus-visible {\n          outline: 3px solid blue;\n          outline-offset: 2px;\n        }\n\n        #modal-close {\n          float: right;\n          background: none;\n          border: 1px solid #ccc;\n          font-size: 16px;\n        }\n\n        #modal-trigger {\n          background-color: #4CAF50;\n          color: white;\n          border: none;\n          font-size: 16px;\n        }\n      &lt;/style&gt;\n\n      &lt;button id=\"modal-trigger\"&gt;Open Modal&lt;/button&gt;\n\n      &lt;div class=\"modal-overlay\"&gt;\n        &lt;div role=\"dialog\" aria-labelledby=\"modal-title\" aria-modal=\"true\" aria-hidden=\"true\" class=\"modal-content\"&gt;\n          &lt;button id=\"modal-close\" aria-label=\"Close dialog\"&gt;×&lt;/button&gt;\n          &lt;h2 id=\"modal-title\"&gt;Accessible Modal Dialog&lt;/h2&gt;\n          &lt;p&gt;This is an example of an accessible modal dialog component.&lt;/p&gt;\n          &lt;p&gt;It traps focus inside the modal, can be closed with the ESC key, and restores focus when closed.&lt;/p&gt;\n          &lt;button&gt;Test Button 1&lt;/button&gt;\n          &lt;button&gt;Test Button 2&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n}\n\ncustomElements.define('accessible-modal', AccessibleModal);\n</code></pre>\n</details>\n<p><strong>Key accessibility features:</strong></p>\n<ul>\n<li>Focus trap within the modal (users can't tab out accidentally)</li>\n<li>Keyboard closing with Escape key</li>\n<li>Focus restoration when modal closes</li>\n<li>Proper ARIA roles (<code>dialog</code>, <code>aria-labelledby</code>, <code>aria-modal</code>)</li>\n<li>Clear visual focus indicators</li>\n</ul>\n<p>Try opening the modal and navigating with Tab. Notice how focus is trapped inside the modal and restored to the trigger button when closed.</p>\n<h3 id=\"3-complex-components-and-keyboard-navigation\">3. Complex Components and Keyboard Navigation<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#3-complex-components-and-keyboard-navigation\" aria-label=\"Link to this section\">#</a></h3>\n<p>While proper keyboard navigation creates a solid foundation, complex components like tabs, accordions, and carousels require specialized interaction patterns to maintain usability.</p>\n<p>The tabs below use <code>tabindex</code> to keep the whole tab list to a single stop in the Tab order. <code>tabindex=\"0\"</code> puts an element in the normal Tab order. <code>tabindex=\"-1\"</code> takes it out of the Tab order but still lets a script focus it with <code>.focus()</code>, which is what the arrow keys do here. Only the selected tab gets <code>0</code>; the others get <code>-1</code>. Tab enters the tab list once, and the arrow keys move around inside it.</p>\n<h4 id=\"interactive-example-accessible-tabs\">Interactive Example: Accessible Tabs<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#interactive-example-accessible-tabs\" aria-label=\"Link to this section\">#</a></h4>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Accessible Tabs Web Component</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;accessible-tabs&gt;&lt;/accessible-tabs&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Simple Dark Mode CSS */\n\n/* Default light mode */\n:root {\n\t--bg-color: #ffffff;\n\t--text-color: #333333;\n}\n\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t}\n}\n\n/* Basic styling */\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tsans-serif;\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 0;\n\ttransition: background-color 0.3s ease, color 0.3s ease;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 100vh;\n\ttext-align: center;\n}\n\n.toggle-button {\n\tpadding: 8px 16px;\n\tborder: 2px solid #333;\n\tborder-radius: 4px;\n\tfont-size: 16px;\n\tcursor: pointer;\n\ttransition: all 0.3s;\n\tbackground: #ccc;\n\tcolor: #000;\n}\n\n.toggle-button.active {\n\tbackground-color: #4caf50;\n\tcolor: white;\n}\n\n.toggle-button.active .toggle-text {\n\tcontent: &quot;ON&quot;;\n}\n\n.active .toggle-text:after {\n\tcontent: &quot;ON&quot;;\n}\n\n.toggle-text:after {\n\tcontent: &quot;OFF&quot;;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class AccessibleTabs extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.render();\n  }\n\n  connectedCallback() {\n    const tabs = Array.from(this.shadowRoot.querySelectorAll(&#39;[role=&quot;tab&quot;]&#39;));\n    const panels = Array.from(this.shadowRoot.querySelectorAll(&#39;[role=&quot;tabpanel&quot;]&#39;));\n\n    // Select first tab by default\n    this.selectTab(tabs[0]);\n\n    tabs.forEach(tab =&gt; {\n      tab.addEventListener(&#39;click&#39;, () =&gt; this.selectTab(tab));\n\n      tab.addEventListener(&#39;keydown&#39;, e =&gt; {\n        if (e.key === &#39;ArrowRight&#39; || e.key === &#39;ArrowLeft&#39;) {\n          e.preventDefault();\n\n          const currentIndex = tabs.indexOf(tab);\n          let nextIndex;\n\n          if (e.key === &#39;ArrowRight&#39;) {\n            nextIndex = (currentIndex + 1) % tabs.length;\n          } else {\n            nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n          }\n\n          tabs[nextIndex].focus();\n          this.selectTab(tabs[nextIndex]);\n        }\n      });\n    });\n  }\n\n  selectTab(selectedTab) {\n    const tabs = Array.from(this.shadowRoot.querySelectorAll(&#39;[role=&quot;tab&quot;]&#39;));\n    const panels = Array.from(this.shadowRoot.querySelectorAll(&#39;[role=&quot;tabpanel&quot;]&#39;));\n\n    tabs.forEach(tab =&gt; {\n      const selected = tab === selectedTab;\n      tab.setAttribute(&#39;aria-selected&#39;, selected);\n      tab.setAttribute(&#39;tabindex&#39;, selected ? &#39;0&#39; : &#39;-1&#39;);\n    });\n\n    panels.forEach(panel =&gt; {\n      const isSelected = panel.id === selectedTab.getAttribute(&#39;aria-controls&#39;);\n      panel.hidden = !isSelected;\n    });\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .tabs {\n          display: flex;\n          border-bottom: 1px solid #ccc;\n        }\n        \n        [role=&quot;tab&quot;] {\n          padding: 10px 15px;\n          margin-right: 5px;\n          background-color: #f1f1f1;\n          border: 1px solid #ccc;\n          border-bottom: none;\n          border-radius: 4px 4px 0 0;\n          cursor: pointer;\n          position: relative;\n          top: 1px;\n        }\n        \n        [role=&quot;tab&quot;][aria-selected=&quot;true&quot;] {\n          background-color: white;\n          border-bottom: 1px solid white;\n        }\n        \n        [role=&quot;tab&quot;]:focus-visible {\n          outline: 3px solid blue;\n          outline-offset: 2px;\n        }\n        \n        [role=&quot;tabpanel&quot;] {\n          padding: 20px;\n          border: 1px solid #ccc;\n          border-top: none;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;tabs-container&quot;&gt;\n        &lt;div role=&quot;tablist&quot; class=&quot;tabs&quot; aria-label=&quot;Programming information&quot;&gt;\n          &lt;button role=&quot;tab&quot; id=&quot;tab-1&quot; aria-selected=&quot;true&quot; aria-controls=&quot;panel-1&quot; tabindex=&quot;0&quot;&gt;HTML&lt;/button&gt;\n          &lt;button role=&quot;tab&quot; id=&quot;tab-2&quot; aria-selected=&quot;false&quot; aria-controls=&quot;panel-2&quot; tabindex=&quot;-1&quot;&gt;CSS&lt;/button&gt;\n          &lt;button role=&quot;tab&quot; id=&quot;tab-3&quot; aria-selected=&quot;false&quot; aria-controls=&quot;panel-3&quot; tabindex=&quot;-1&quot;&gt;JavaScript&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div role=&quot;tabpanel&quot; id=&quot;panel-1&quot; aria-labelledby=&quot;tab-1&quot;&gt;\n          &lt;h3&gt;HTML&lt;/h3&gt;\n          &lt;p&gt;HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.&lt;/p&gt;\n        &lt;/div&gt;\n        \n        &lt;div role=&quot;tabpanel&quot; id=&quot;panel-2&quot; aria-labelledby=&quot;tab-2&quot; hidden&gt;\n          &lt;h3&gt;CSS&lt;/h3&gt;\n          &lt;p&gt;CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in HTML.&lt;/p&gt;\n        &lt;/div&gt;\n        \n        &lt;div role=&quot;tabpanel&quot; id=&quot;panel-3&quot; aria-labelledby=&quot;tab-3&quot; hidden&gt;\n          &lt;h3&gt;JavaScript&lt;/h3&gt;\n          &lt;p&gt;JavaScript is a programming language that enables interactive web pages and is an essential part of web applications.&lt;/p&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\ncustomElements.define(&#39;accessible-tabs&#39;, AccessibleTabs);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<details>\n<summary>Accessible tabs JavaScript · 131 lines</summary>\n<pre><code class=\"language-javascript\">class AccessibleTabs extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.render();\n  }\n\n  connectedCallback() {\n    const tabs = Array.from(this.shadowRoot.querySelectorAll('[role=\"tab\"]'));\n    const panels = Array.from(this.shadowRoot.querySelectorAll('[role=\"tabpanel\"]'));\n\n    // Select first tab by default\n    this.selectTab(tabs[0]);\n\n    tabs.forEach(tab =&gt; {\n      tab.addEventListener('click', () =&gt; this.selectTab(tab));\n\n      tab.addEventListener('keydown', e =&gt; {\n        // Arrow key navigation\n        if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {\n          e.preventDefault();\n\n          const currentIndex = tabs.indexOf(tab);\n          let nextIndex;\n\n          if (e.key === 'ArrowRight') {\n            nextIndex = (currentIndex + 1) % tabs.length;\n          } else {\n            nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n          }\n\n          tabs[nextIndex].focus();\n          this.selectTab(tabs[nextIndex]);\n        }\n\n        // Home and End keys for first/last tabs\n        if (e.key === 'Home') {\n          e.preventDefault();\n          tabs[0].focus();\n          this.selectTab(tabs[0]);\n        }\n\n        if (e.key === 'End') {\n          e.preventDefault();\n          tabs[tabs.length - 1].focus();\n          this.selectTab(tabs[tabs.length - 1]);\n        }\n      });\n    });\n  }\n\n  selectTab(selectedTab) {\n    const tabs = Array.from(this.shadowRoot.querySelectorAll('[role=\"tab\"]'));\n    const panels = Array.from(this.shadowRoot.querySelectorAll('[role=\"tabpanel\"]'));\n\n    tabs.forEach(tab =&gt; {\n      const selected = tab === selectedTab;\n      tab.setAttribute('aria-selected', selected);\n      tab.setAttribute('tabindex', selected ? '0' : '-1');\n    });\n\n    panels.forEach(panel =&gt; {\n      const isSelected = panel.id === selectedTab.getAttribute('aria-controls');\n      panel.hidden = !isSelected;\n    });\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .tabs {\n          display: flex;\n          border-bottom: 1px solid #ccc;\n        }\n\n        [role=\"tab\"] {\n          padding: 10px 15px;\n          margin-right: 5px;\n          background-color: #f1f1f1;\n          border: 1px solid #ccc;\n          border-bottom: none;\n          border-radius: 4px 4px 0 0;\n          cursor: pointer;\n          position: relative;\n          top: 1px;\n        }\n\n        [role=\"tab\"][aria-selected=\"true\"] {\n          background-color: white;\n          border-bottom: 1px solid white;\n        }\n\n        [role=\"tab\"]:focus-visible {\n          outline: 3px solid blue;\n          outline-offset: 2px;\n        }\n\n        [role=\"tabpanel\"] {\n          padding: 20px;\n          border: 1px solid #ccc;\n          border-top: none;\n        }\n      &lt;/style&gt;\n\n      &lt;div class=\"tabs-container\"&gt;\n        &lt;div role=\"tablist\" class=\"tabs\" aria-label=\"Programming information\"&gt;\n          &lt;button role=\"tab\" id=\"tab-1\" aria-selected=\"true\" aria-controls=\"panel-1\" tabindex=\"0\"&gt;HTML&lt;/button&gt;\n          &lt;button role=\"tab\" id=\"tab-2\" aria-selected=\"false\" aria-controls=\"panel-2\" tabindex=\"-1\"&gt;CSS&lt;/button&gt;\n          &lt;button role=\"tab\" id=\"tab-3\" aria-selected=\"false\" aria-controls=\"panel-3\" tabindex=\"-1\"&gt;JavaScript&lt;/button&gt;\n        &lt;/div&gt;\n\n        &lt;div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\"&gt;\n          &lt;h3&gt;HTML&lt;/h3&gt;\n          &lt;p&gt;HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.&lt;/p&gt;\n        &lt;/div&gt;\n\n        &lt;div role=\"tabpanel\" id=\"panel-2\" aria-labelledby=\"tab-2\" hidden&gt;\n          &lt;h3&gt;CSS&lt;/h3&gt;\n          &lt;p&gt;CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in HTML.&lt;/p&gt;\n        &lt;/div&gt;\n\n        &lt;div role=\"tabpanel\" id=\"panel-3\" aria-labelledby=\"tab-3\" hidden&gt;\n          &lt;h3&gt;JavaScript&lt;/h3&gt;\n          &lt;p&gt;JavaScript is a programming language that enables interactive web pages and is an essential part of web applications.&lt;/p&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n}\n\ncustomElements.define('accessible-tabs', AccessibleTabs);\n</code></pre>\n</details>\n<p><strong>Key accessibility features:</strong></p>\n<ul>\n<li>Proper ARIA roles for <code>tablist</code>, <code>tab</code>, and <code>tabpanel</code></li>\n<li>Keyboard navigation with arrow keys, Home, and End</li>\n<li>Single tab stop in the tab order with arrow key navigation between tabs</li>\n<li>Proper state management with <code>aria-selected</code> and <code>tabindex</code></li>\n<li>Clear relationship between tabs and panels using <code>aria-controls</code> and <code>aria-labelledby</code></li>\n</ul>\n<p>Try using the arrow keys to navigate between tabs. Notice how this follows the expected keyboard interaction pattern for tabs.</p>\n<h3 id=\"4-announcements-for-dynamic-content\">4. Announcements for Dynamic Content<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#4-announcements-for-dynamic-content\" aria-label=\"Link to this section\">#</a></h3>\n<p>Beyond static structure and interaction, many modern interfaces feature dynamic content that changes without page reloads. This presents another accessibility challenge: how do we keep non-visual users informed of these changes?</p>\n<p>Think of Sarah pressing a button that adds an item to a cart. Her focus stays on the button, and a \"1 item in cart\" message appears elsewhere on the screen. Nothing told her. An element marked <code>aria-live</code> is a <strong>live region</strong>: when its text changes, the screen reader speaks the new text even though focus is somewhere else. <code>aria-live=\"polite\"</code> waits for a pause in speech; <code>aria-live=\"assertive\"</code> interrupts, so save it for urgent messages. The region should already be on the page before you change its text.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Live Region Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;live-region&gt;&lt;/live-region&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Simple Dark Mode CSS */\n\n/* Default light mode */\n:root {\n\t--bg-color: #ffffff;\n\t--text-color: #333333;\n}\n\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t}\n}\n\n/* Basic styling */\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tsans-serif;\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 0;\n\ttransition: background-color 0.3s ease, color 0.3s ease;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 100vh;\n\ttext-align: center;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class LiveRegion extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.render();\n  }\n\n  connectedCallback() {\n    const button = this.shadowRoot.querySelector(&#39;button&#39;);\n    const liveRegion = this.shadowRoot.querySelector(&#39;[aria-live]&#39;);\n    let count = 0;\n\n    // Listen for theme changes\n    this.updateTheme = () =&gt; {\n      const isDarkMode = this.shadowRoot.host.hasAttribute(&#39;dark-mode&#39;);\n      this.shadowRoot.querySelector(&#39;.container&#39;).classList.toggle(&#39;dark-theme&#39;, isDarkMode);\n    };\n\n    // Check for system preference initially\n    const prefersDarkMode = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches;\n    if (prefersDarkMode) {\n      this.shadowRoot.host.setAttribute(&#39;dark-mode&#39;, &#39;&#39;);\n      this.updateTheme();\n    }\n\n    // Listen for system preference changes\n    window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n      if (e.matches) {\n        this.shadowRoot.host.setAttribute(&#39;dark-mode&#39;, &#39;&#39;);\n      } else {\n        this.shadowRoot.host.removeAttribute(&#39;dark-mode&#39;);\n      }\n      this.updateTheme();\n    });\n\n    // Button click event\n    button.addEventListener(&#39;click&#39;, () =&gt; {\n      count++;\n      liveRegion.textContent = `Button clicked ${count} time${count === 1 ? &#39;&#39; : &#39;s&#39;}`;\n    });\n\n    // Theme toggle event\n    //     themeToggle.addEventListener(&#39;click&#39;, () =&gt; {\n    //       if (this.shadowRoot.host.hasAttribute(&#39;dark-mode&#39;)) {\n    //         this.shadowRoot.host.removeAttribute(&#39;dark-mode&#39;);\n    //       } else {\n    //         this.shadowRoot.host.setAttribute(&#39;dark-mode&#39;, &#39;&#39;);\n    //       }\n    //       this.updateTheme();\n\n    //       // Announce theme change to screen readers\n    //       const mode = this.shadowRoot.host.hasAttribute(&#39;dark-mode&#39;) ? &#39;dark&#39; : &#39;light&#39;;\n    //       const announcement = this.shadowRoot.querySelector(&#39;.theme-announcement&#39;);\n    //       announcement.textContent = `Theme changed to ${mode} mode`;\n    //     });\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .container {\n          padding: 20px;\n          border-radius: 8px;\n          font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n          background-color: #ffffff;\n          color: #333333;\n          transition: background-color 0.3s, color 0.3s;\n        }\n        \n        .container.dark-theme {\n          background-color: #1a1a1a;\n          color: #e0e0e0;\n        }\n        \n        button {\n          padding: 8px 16px;\n          background-color: #4CAF50;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n          font-size: 16px;\n          transition: background-color 0.3s, transform 0.1s;\n        }\n        \n        .dark-theme button {\n          background-color: #3a8a3d;\n        }\n        \n        button:hover {\n          opacity: 0.9;\n        }\n        \n        button:active {\n          transform: scale(0.98);\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #7cc0ff;\n          outline-offset: 2px;\n        }\n        \n        .dark-theme button:focus-visible {\n          outline-color: #4a8dcb;\n        }\n        \n        #theme-toggle {\n          background-color: #6c757d;\n          margin-left: 10px;\n        }\n        \n        .dark-theme #theme-toggle {\n          background-color: #495057;\n        }\n        \n        .live-region {\n          margin-top: 15px;\n          padding: 12px;\n          background-color: #f8f8f8;\n          border-radius: 6px;\n          border-left: 4px solid #4CAF50;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.05);\n          font-size: 16px;\n        }\n        \n        .dark-theme .live-region {\n          background-color: #2a2a2a;\n          border-left-color: #3a8a3d;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.2);\n        }\n        \n        .hidden {\n          position: absolute;\n          width: 1px;\n          height: 1px;\n          padding: 0;\n          margin: -1px;\n          overflow: hidden;\n          clip: rect(0, 0, 0, 0);\n          white-space: nowrap;\n          border-width: 0;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;container&quot;&gt;\n        &lt;div class=&quot;controls&quot;&gt;\n          &lt;button&gt;Increment Counter&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;live-region&quot; aria-live=&quot;polite&quot;&gt;Counter starts at 0&lt;/div&gt;\n        \n        &lt;!-- Hidden live region for theme announcements --&gt;\n        &lt;div class=&quot;theme-announcement hidden&quot; aria-live=&quot;polite&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\ncustomElements.define(&#39;live-region&#39;, LiveRegion);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<pre><code class=\"language-javascript\">class LiveRegion extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.render();\n  }\n\n  connectedCallback() {\n    const button = this.shadowRoot.querySelector('button');\n    const liveRegion = this.shadowRoot.querySelector('[aria-live]');\n    let count = 0;\n\n    button.addEventListener('click', () =&gt; {\n      count++;\n      liveRegion.textContent = `Button clicked ${count} time${count === 1 ? '' : 's'}`;\n    });\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        button {\n          padding: 8px 16px;\n          background-color: #4CAF50;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          cursor: pointer;\n        }\n\n        button:focus-visible {\n          outline: 3px solid blue;\n          outline-offset: 2px;\n        }\n\n        .live-region {\n          margin-top: 10px;\n          padding: 10px;\n          background-color: #f8f8f8;\n          border-radius: 4px;\n        }\n      &lt;/style&gt;\n\n      &lt;div&gt;\n        &lt;button&gt;Increment Counter&lt;/button&gt;\n        &lt;div class=\"live-region\" aria-live=\"polite\"&gt;Counter starts at 0&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n}\n\ncustomElements.define('live-region', LiveRegion);\n</code></pre>\n<p><strong>Key accessibility feature:</strong></p>\n<ul>\n<li>Uses <code>aria-live=\"polite\"</code> to announce changes to screen readers without interrupting current announcements</li>\n</ul>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>Focus stays on a button while the page changes. Which of these updates needs an <code>aria-live</code> region for a screen reader user to hear about it?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Pressing Tab moves focus to the next link</li>\n<li>A dialog opens and the script moves focus onto its heading</li>\n<li>A \"Saved\" message appears next to the button while focus stays on the button</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: C. A \"Saved\" message appears next to the button while focus stays on the button</p>\n<p>Moving focus already makes the screen reader speak the newly focused element, so the first two announce themselves. A message that appears elsewhere while focus stays put is silent unless it sits in a live region.</p>\n</div>\n</div>\n</section>\n<h2 id=\"testing-for-accessibility\">Testing for Accessibility<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#testing-for-accessibility\" aria-label=\"Link to this section\">#</a></h2>\n<p>Implementing these accessibility features is only half the journey. To ensure your components work reliably for all users, you need a systematic approach to testing. Let's explore the tools and techniques that will help you verify your implementation meets accessibility standards.</p>\n<h3 id=\"1-automated-testing\">1. Automated Testing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#1-automated-testing\" aria-label=\"Link to this section\">#</a></h3>\n<p>Utilize tools like <a href=\"https://www.deque.com/axe/\">axe-core</a> or <a href=\"https://developers.google.com/web/tools/lighthouse\">Lighthouse</a>:</p>\n<pre><code class=\"language-javascript\">// Example of using axe-core for accessibility testing\nimport axe from 'axe-core';\n\n// Test a specific web component\naxe.run(document.querySelector('accessible-tabs'), (err, results) =&gt; {\n  if (err) {\n    console.error('Accessibility testing failed:', err);\n  } else {\n    console.log('Accessibility test results:', results);\n  }\n});\n</code></pre>\n<p>Lighthouse is an open-source automated tool developed by Google that audits web pages for performance, accessibility, SEO, and more. There are three ways to run it for accessibility:</p>\n<p><strong>Method 1: Using Chrome DevTools</strong></p>\n<ol>\n<li>Open Chrome browser</li>\n<li>Navigate to the webpage you want to test</li>\n<li>Press F12 or right-click and select \"Inspect\" to open DevTools</li>\n<li>Click on the \"Lighthouse\" tab</li>\n<li>Select \"Accessibility\" checkbox (deselect others if you only want accessibility)</li>\n<li>Click \"Generate report\"</li>\n</ol>\n<p><strong>Method 2: Chrome Extension</strong></p>\n<ol>\n<li>Install the Lighthouse extension from Chrome Web Store</li>\n<li>Navigate to the page you want to test</li>\n<li>Click the Lighthouse icon in your browser toolbar</li>\n<li>Select \"Accessibility\" checkbox</li>\n<li>Click \"Generate report\"</li>\n</ol>\n<p><strong>Method 3: Command Line</strong></p>\n<pre><code class=\"language-bash\"># Install Lighthouse globally\nnpm install -g lighthouse\n\n# Run Lighthouse accessibility audit\nlighthouse https://example.com --only-categories=accessibility --output=html --output-path=./report.html\n</code></pre>\n<h3 id=\"2-manual-testing\">2. Manual Testing<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#2-manual-testing\" aria-label=\"Link to this section\">#</a></h3>\n<p>Automated tests can't catch everything. Manual testing should include:</p>\n<p><strong>Keyboard Testing:</strong></p>\n<ul>\n<li>Can all interactive elements be accessed via Tab key?</li>\n<li>Do all controls work with keyboard alternatives (Enter, Space, arrow keys)?</li>\n<li>Is there a visible focus indicator at all times?</li>\n</ul>\n<p><strong>Screen Reader Testing:</strong></p>\n<ul>\n<li>Try your components with VoiceOver (macOS), NVDA or JAWS (Windows), or TalkBack (Android)</li>\n<li>Is all relevant information announced?</li>\n<li>Are state changes communicated properly?</li>\n</ul>\n<p><strong>High Contrast Mode:</strong></p>\n<ul>\n<li>Test with high contrast mode enabled in your OS</li>\n<li>Do all interactive elements remain visible and distinct?</li>\n</ul>\n<h3 id=\"3-optional-a-screen-reader-simulator\">3. Optional: A Screen Reader Simulator<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#3-optional-a-screen-reader-simulator\" aria-label=\"Link to this section\">#</a></h3>\n<p><em>This section is optional. The simulator below is long, so you can skip to \"Testing with NVDA\" and lose nothing the rest of the post depends on.</em> This interactive example demonstrates how screen readers interpret web content, without requiring any software installation. Simply enter your HTML or use the built-in generator to compare non-semantic versus semantic markup. See firsthand how thoughtful HTML structure dramatically changes the experience for users who rely on assistive technology. Test your own code to identify potential accessibility improvements. Note: This simulator provides educational insights only and should not replace testing with actual screen reader technology when evaluating web accessibility.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Screen Reader Simulator</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Accessibility Screen Reader Simulator&lt;/h1&gt;\n  &lt;p&gt;Test how a screen reader would interpret your HTML content&lt;/p&gt;\n  \n  &lt;!-- Use the web component anywhere in your HTML --&gt;\n  &lt;a11y-tester&gt;&lt;/a11y-tester&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// A11y Tester Web Component\n// This file defines a custom HTML element &lt;a11y-tester&gt; that can be used anywhere in your HTML\n\nclass AccessibilityTester extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create styles\n    const style = document.createElement(&#39;style&#39;);\n    style.textContent = `\n        :host {\n          display: block;\n          font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n          border: 1px solid #ddd;\n          border-radius: 5px;\n          overflow: hidden;\n        }\n        \n        .container {\n          display: flex;\n          flex-direction: column;\n          min-height: 400px;\n        }\n        \n        .input-section, .output-section {\n          padding: 15px;\n        }\n        \n        .input-section {\n          background-color: white;\n          border-bottom: 1px solid #ddd;\n        }\n        \n        .output-section {\n          background-color: #f9f9f9;\n        }\n        \n        h2 {\n          margin-top: 0;\n          margin-bottom: 15px;\n          font-size: 18px;\n          color: #2c3e50;\n        }\n        \n        textarea {\n          width: 100%;\n          min-height: 150px;\n          padding: 10px;\n          border: 1px solid #ddd;\n          border-radius: 4px;\n          font-family: monospace;\n          font-size: 14px;\n          resize: vertical;\n        }\n        \n        button {\n          background-color: #3498db;\n          color: white;\n          border: none;\n          padding: 8px 16px;\n          border-radius: 4px;\n          margin-top: 10px;\n          cursor: pointer;\n          font-weight: 500;\n          margin-right: 8px;\n        }\n        \n        button:hover {\n          background-color: #2980b9;\n        }\n        \n        button:disabled {\n          background-color: #95a5a6;\n          cursor: not-allowed;\n        }\n        \n        .controls {\n          display: flex;\n        }\n        \n        .a11y-result {\n          border: 1px solid #ddd;\n          border-radius: 4px;\n          margin-bottom: 15px;\n          background-color: white;\n        }\n        \n        .a11y-result-header {\n          padding: 8px 12px;\n          background-color: #e9e9e9;\n          font-weight: 500;\n          border-bottom: 1px solid #ddd;\n        }\n        \n        .a11y-result-content {\n          padding: 12px;\n        }\n        \n        .a11y-element {\n          padding: 10px;\n          margin-bottom: 8px;\n          border-left: 3px solid #3498db;\n          background-color: rgba(52, 152, 219, 0.1);\n        }\n        \n        .a11y-element.speaking {\n          background-color: rgba(46, 204, 113, 0.2);\n          border-left-color: #2ecc71;\n        }\n        \n        .a11y-element-type {\n          font-weight: 500;\n          margin-bottom: 5px;\n        }\n        \n        .a11y-element-announcement {\n          font-family: monospace;\n          color: #333;\n        }\n        \n        .a11y-warnings {\n          margin-top: 15px;\n        }\n        \n        .a11y-warning {\n          padding: 8px 12px;\n          margin-bottom: 8px;\n          background-color: rgba(243, 156, 18, 0.1);\n          border-left: 3px solid #f39c12;\n        }\n        \n        .sample-buttons {\n          display: flex;\n          margin-top: 10px;\n          margin-bottom: 10px;\n        }\n        \n        .sample-button {\n          background-color: #2ecc71;\n          margin-right: 8px;\n        }\n        \n        .sample-button:hover {\n          background-color: #27ae60;\n        }\n        \n        .info-text {\n          font-size: 13px;\n          color: #7f8c8d;\n          margin-top: 5px;\n          font-style: italic;\n        }\n      `;\n\n    // Create HTML structure\n    const container = document.createElement(&#39;div&#39;);\n    container.className = &#39;container&#39;;\n\n    // Input section\n    const inputSection = document.createElement(&#39;div&#39;);\n    inputSection.className = &#39;input-section&#39;;\n\n    const inputTitle = document.createElement(&#39;h2&#39;);\n    inputTitle.textContent = &#39;HTML Input&#39;;\n\n    // Sample buttons\n    const sampleButtons = document.createElement(&#39;div&#39;);\n    sampleButtons.className = &#39;sample-buttons&#39;;\n\n    this.nonSemanticButton = document.createElement(&#39;button&#39;);\n    this.nonSemanticButton.className = &#39;sample-button non-semantic-button&#39;;\n    this.nonSemanticButton.textContent = &#39;Insert Non-Semantic Example&#39;;\n\n    this.semanticButton = document.createElement(&#39;button&#39;);\n    this.semanticButton.className = &#39;sample-button semantic-button&#39;;\n    this.semanticButton.textContent = &#39;Insert Semantic Example&#39;;\n\n    sampleButtons.appendChild(this.nonSemanticButton);\n    sampleButtons.appendChild(this.semanticButton);\n\n    const infoText = document.createElement(&#39;div&#39;);\n    infoText.className = &#39;info-text&#39;;\n    infoText.textContent = &#39;Compare how screen readers interpret semantic vs. non-semantic HTML&#39;;\n\n    this.htmlInput = document.createElement(&#39;textarea&#39;);\n    this.htmlInput.placeholder = &#39;Paste your HTML here or use sample buttons above...&#39;;\n\n    const controls = document.createElement(&#39;div&#39;);\n    controls.className = &#39;controls&#39;;\n\n    this.testButton = document.createElement(&#39;button&#39;);\n    this.testButton.className = &#39;test-button&#39;;\n    this.testButton.textContent = &#39;Test Accessibility&#39;;\n\n    this.playButton = document.createElement(&#39;button&#39;);\n    this.playButton.className = &#39;play-button&#39;;\n    this.playButton.textContent = &#39;Simulate Screen Reader&#39;;\n    this.playButton.disabled = true;\n\n    this.stopButton = document.createElement(&#39;button&#39;);\n    this.stopButton.className = &#39;stop-button&#39;;\n    this.stopButton.textContent = &#39;Stop&#39;;\n    this.stopButton.disabled = true;\n\n    controls.appendChild(this.testButton);\n    controls.appendChild(this.playButton);\n    controls.appendChild(this.stopButton);\n\n    inputSection.appendChild(inputTitle);\n    inputSection.appendChild(sampleButtons);\n    inputSection.appendChild(infoText);\n    inputSection.appendChild(this.htmlInput);\n    inputSection.appendChild(controls);\n\n    // Output section\n    const outputSection = document.createElement(&#39;div&#39;);\n    outputSection.className = &#39;output-section&#39;;\n\n    const outputTitle = document.createElement(&#39;h2&#39;);\n    outputTitle.textContent = &#39;Screen Reader Simulation&#39;;\n\n    this.outputContainer = document.createElement(&#39;div&#39;);\n    this.outputContainer.className = &#39;output-container&#39;;\n\n    this.warningsContainer = document.createElement(&#39;div&#39;);\n    this.warningsContainer.className = &#39;a11y-warnings&#39;;\n\n    outputSection.appendChild(outputTitle);\n    outputSection.appendChild(this.outputContainer);\n    outputSection.appendChild(this.warningsContainer);\n\n    // Assemble all parts\n    container.appendChild(inputSection);\n    container.appendChild(outputSection);\n\n    // Attach to shadow DOM\n    this.shadowRoot.appendChild(style);\n    this.shadowRoot.appendChild(container);\n\n    // State\n    this.accessibilityOutput = [];\n    this.isSpeaking = false;\n    this.currentSpeakingIndex = -1;\n    this.speechSynthesis = window.speechSynthesis;\n\n    // Bind event handlers\n    this.testButton.addEventListener(&#39;click&#39;, () =&gt; this.processHtml());\n    this.playButton.addEventListener(&#39;click&#39;, () =&gt; this.startSpeaking());\n    this.stopButton.addEventListener(&#39;click&#39;, () =&gt; this.stopSpeaking());\n    this.nonSemanticButton.addEventListener(&#39;click&#39;, () =&gt; this.insertNonSemanticExample());\n    this.semanticButton.addEventListener(&#39;click&#39;, () =&gt; this.insertSemanticExample());\n  }\n\n  processHtml() {\n    const html = this.htmlInput.value.trim();\n    if (!html) {\n      this.showWarning(&#39;Please enter some HTML to test&#39;);\n      return;\n    }\n\n    // Clear previous results\n    this.accessibilityOutput = [];\n    this.outputContainer.innerHTML = &#39;&#39;;\n    this.warningsContainer.innerHTML = &#39;&#39;;\n\n    // Create a temporary DOM element to parse the HTML\n    const tempContainer = document.createElement(&#39;div&#39;);\n    tempContainer.innerHTML = html;\n\n    // Process the DOM tree to extract accessibility information\n    this.processNode(tempContainer, this.accessibilityOutput);\n\n    // Render the results\n    this.renderResults();\n\n    // Enable play button if we have results\n    this.playButton.disabled = this.accessibilityOutput.length === 0;\n  }\n\n  processNode(node, results) {\n    // Skip comment nodes and script tags\n    if (node.nodeType === Node.COMMENT_NODE ||\n    node.tagName &amp;&amp; node.tagName.toLowerCase() === &#39;script&#39;) {\n      return;\n    }\n\n    // Process text nodes with non-empty content\n    if (node.nodeType === Node.TEXT_NODE) {\n      const text = node.textContent.trim();\n      if (text) {\n        results.push({\n          type: &#39;text&#39;,\n          announcement: text,\n          node: node });\n\n      }\n      return;\n    }\n\n    // Process element nodes\n    if (node.nodeType === Node.ELEMENT_NODE) {\n      // Get accessibility attributes\n      const role = node.getAttribute(&#39;role&#39;);\n      const ariaLabel = node.getAttribute(&#39;aria-label&#39;);\n      const ariaLabelledby = node.getAttribute(&#39;aria-labelledby&#39;);\n      const ariaHidden = node.getAttribute(&#39;aria-hidden&#39;) === &#39;true&#39;;\n      const ariaDescribedby = node.getAttribute(&#39;aria-describedby&#39;);\n\n      // Skip hidden elements\n      if (ariaHidden) {\n        return;\n      }\n\n      // Extract relevant info based on element type\n      const tagName = node.tagName.toLowerCase();\n      let elementInfo = { tagName };\n      let announcement = &#39;&#39;;\n      let warnings = [];\n\n      switch (tagName) {\n        case &#39;img&#39;:\n          const alt = node.getAttribute(&#39;alt&#39;);\n          if (alt === null || alt === undefined) {\n            warnings.push(&#39;Image is missing alt attribute&#39;);\n            announcement = &#39;Unlabelled image&#39;;\n          } else if (alt === &#39;&#39;) {\n            // Empty alt is valid for decorative images\n            announcement = &#39;&#39;; // Screen readers skip these\n          } else {\n            announcement = `Image: ${alt}`;\n          }\n          elementInfo = { ...elementInfo, type: &#39;image&#39;, alt };\n          break;\n\n        case &#39;a&#39;:\n          const linkText = node.textContent.trim();\n          const href = node.getAttribute(&#39;href&#39;) || &#39;&#39;;\n\n          if (!linkText) {\n            warnings.push(&#39;Link has no text content&#39;);\n          }\n\n          if (!href) {\n            warnings.push(&#39;Link is missing href attribute&#39;);\n          }\n\n          announcement = `Link: ${ariaLabel || linkText || &#39;Unlabelled link&#39;}`;\n          if (href) {\n            announcement += ` to ${href}`;\n          }\n\n          elementInfo = { ...elementInfo, type: &#39;link&#39;, text: linkText, href };\n          break;\n\n        case &#39;button&#39;:\n          const buttonText = node.textContent.trim();\n\n          if (!buttonText &amp;&amp; !ariaLabel) {\n            warnings.push(&#39;Button has no text content or aria-label&#39;);\n          }\n\n          announcement = `Button: ${ariaLabel || buttonText || &#39;Unlabelled button&#39;}`;\n          elementInfo = { ...elementInfo, type: &#39;button&#39;, text: buttonText };\n          break;\n\n        case &#39;input&#39;:\n          const inputType = node.getAttribute(&#39;type&#39;) || &#39;text&#39;;\n          const placeholder = node.getAttribute(&#39;placeholder&#39;) || &#39;&#39;;\n          const label = this.findLabelForInput(node);\n          const name = node.getAttribute(&#39;name&#39;) || &#39;&#39;;\n          const required = node.hasAttribute(&#39;required&#39;);\n\n          if (!label &amp;&amp; !ariaLabel) {\n            warnings.push(&#39;Input field is missing an associated label or aria-label&#39;);\n          }\n\n          let inputAnnouncement = &#39;&#39;;\n          switch (inputType) {\n            case &#39;text&#39;:\n              inputAnnouncement = `Text field: ${ariaLabel || label || name || &#39;Unlabelled field&#39;}`;\n              break;\n            case &#39;checkbox&#39;:\n              inputAnnouncement = `Checkbox: ${ariaLabel || label || name || &#39;Unlabelled checkbox&#39;}`;\n              inputAnnouncement += node.checked ? &#39;, checked&#39; : &#39;, not checked&#39;;\n              break;\n            case &#39;radio&#39;:\n              inputAnnouncement = `Radio button: ${ariaLabel || label || name || &#39;Unlabelled radio button&#39;}`;\n              inputAnnouncement += node.checked ? &#39;, selected&#39; : &#39;, not selected&#39;;\n              break;\n            case &#39;submit&#39;:\n              inputAnnouncement = `Submit button: ${node.value || &#39;Submit&#39;}`;\n              break;\n            default:\n              inputAnnouncement = `${inputType} field: ${ariaLabel || label || name || &#39;Unlabelled field&#39;}`;}\n\n\n          if (required) {\n            inputAnnouncement += &#39;, required&#39;;\n          }\n\n          announcement = inputAnnouncement;\n          elementInfo = {\n            ...elementInfo,\n            type: &#39;input&#39;,\n            inputType,\n            label,\n            placeholder,\n            required };\n\n          break;\n\n        case &#39;select&#39;:\n          const selectLabel = this.findLabelForInput(node);\n\n          if (!selectLabel &amp;&amp; !ariaLabel) {\n            warnings.push(&#39;Select dropdown is missing an associated label or aria-label&#39;);\n          }\n\n          announcement = `Dropdown menu: ${ariaLabel || selectLabel || &#39;Unlabelled dropdown&#39;}`;\n          elementInfo = { ...elementInfo, type: &#39;select&#39;, label: selectLabel };\n          break;\n\n        case &#39;h1&#39;:\n        case &#39;h2&#39;:\n        case &#39;h3&#39;:\n        case &#39;h4&#39;:\n        case &#39;h5&#39;:\n        case &#39;h6&#39;:\n          const level = tagName.charAt(1);\n          const headingText = node.textContent.trim();\n\n          if (!headingText) {\n            warnings.push(`Heading (${tagName}) has no text content`);\n          }\n\n          announcement = `Heading level ${level}: ${headingText || &#39;Empty heading&#39;}`;\n          elementInfo = { ...elementInfo, type: &#39;heading&#39;, level, text: headingText };\n          break;\n\n        case &#39;table&#39;:\n          const caption = node.querySelector(&#39;caption&#39;);\n          const captionText = caption ? caption.textContent.trim() : &#39;&#39;;\n\n          if (!captionText &amp;&amp; !ariaLabel) {\n            warnings.push(&#39;Table is missing a caption or aria-label&#39;);\n          }\n\n          announcement = `Table: ${ariaLabel || captionText || &#39;Unlabelled table&#39;}`;\n          elementInfo = { ...elementInfo, type: &#39;table&#39;, caption: captionText };\n          break;\n\n        case &#39;figure&#39;:\n          const figcaption = node.querySelector(&#39;figcaption&#39;);\n          const figcaptionText = figcaption ? figcaption.textContent.trim() : &#39;&#39;;\n\n          if (!figcaptionText &amp;&amp; !ariaLabel) {\n            warnings.push(&#39;Figure is missing a figcaption or aria-label&#39;);\n          }\n\n          announcement = `Figure: ${ariaLabel || figcaptionText || &#39;Unlabelled figure&#39;}`;\n          elementInfo = { ...elementInfo, type: &#39;figure&#39;, caption: figcaptionText };\n\n          // Process children separately to maintain context\n          const children = Array.from(node.children);\n          children.forEach(child =&gt; this.processNode(child, results));\n\n          // Skip processing children again\n          return;\n\n        case &#39;ul&#39;:\n          announcement = `Unordered list with ${node.children.length} items`;\n          elementInfo = { ...elementInfo, type: &#39;list&#39;, listType: &#39;unordered&#39; };\n          break;\n\n        case &#39;ol&#39;:\n          announcement = `Ordered list with ${node.children.length} items`;\n          elementInfo = { ...elementInfo, type: &#39;list&#39;, listType: &#39;ordered&#39; };\n          break;\n\n        case &#39;li&#39;:\n          const listItemText = node.textContent.trim();\n          let prefix = &#39;&#39;;\n\n          if (node.parentElement) {\n            if (node.parentElement.tagName.toLowerCase() === &#39;ol&#39;) {\n              // Find index of this li element among its siblings\n              const siblings = Array.from(node.parentElement.children);\n              const index = siblings.indexOf(node) + 1;\n              prefix = `Item ${index}: `;\n            } else {\n              prefix = &#39;Bullet: &#39;;\n            }\n          }\n\n          announcement = `${prefix}${listItemText}`;\n          elementInfo = { ...elementInfo, type: &#39;listItem&#39;, text: listItemText };\n          break;\n\n        case &#39;form&#39;:\n          announcement = `Form: ${ariaLabel || &#39;Unlabelled form&#39;}`;\n          elementInfo = { ...elementInfo, type: &#39;form&#39; };\n          break;\n\n        // Add more element types as needed\n\n        default:\n          // For other elements, only process if they have meaningful accessibility attributes\n          if (ariaLabel) {\n            announcement = ariaLabel;\n            elementInfo = { ...elementInfo, type: &#39;custom&#39;, role };\n          } else if (role) {\n            announcement = `${role}: ${node.textContent.trim()}`;\n            elementInfo = { ...elementInfo, type: &#39;custom&#39;, role };\n          }}\n\n\n      // Add result if we have an announcement\n      if (announcement) {\n        results.push({\n          ...elementInfo,\n          announcement,\n          warnings,\n          node });\n\n      }\n\n      // Recursively process child nodes\n      const children = Array.from(node.childNodes);\n      children.forEach(child =&gt; this.processNode(child, results));\n    }\n  }\n\n  findLabelForInput(input) {\n    // Check for label with matching &#39;for&#39; attribute\n    if (input.id) {\n      const label = document.querySelector(`label[for=&quot;${input.id}&quot;]`);\n      if (label) {\n        return label.textContent.trim();\n      }\n    }\n\n    // Check if input is wrapped in a label\n    let parent = input.parentElement;\n    while (parent) {\n      if (parent.tagName.toLowerCase() === &#39;label&#39;) {\n        // Get text content excluding the input itself\n        const clone = parent.cloneNode(true);\n        const inputInClone = clone.querySelector(&#39;input, select, textarea&#39;);\n        if (inputInClone) {\n          inputInClone.remove();\n        }\n        return clone.textContent.trim();\n      }\n      parent = parent.parentElement;\n    }\n\n    return &#39;&#39;;\n  }\n\n  renderResults() {\n    if (this.accessibilityOutput.length === 0) {\n      this.outputContainer.innerHTML = &#39;&lt;p&gt;No accessibility information found or HTML is empty.&lt;/p&gt;&#39;;\n      return;\n    }\n\n    const resultElement = document.createElement(&#39;div&#39;);\n    resultElement.className = &#39;a11y-result&#39;;\n\n    const resultHeader = document.createElement(&#39;div&#39;);\n    resultHeader.className = &#39;a11y-result-header&#39;;\n    resultHeader.textContent = &#39;Screen Reader Navigation Sequence&#39;;\n    resultElement.appendChild(resultHeader);\n\n    const resultContent = document.createElement(&#39;div&#39;);\n    resultContent.className = &#39;a11y-result-content&#39;;\n\n    // Create elements for each result\n    this.accessibilityOutput.forEach((item, index) =&gt; {\n      const element = document.createElement(&#39;div&#39;);\n      element.className = &#39;a11y-element&#39;;\n      element.dataset.index = index;\n\n      const elementType = document.createElement(&#39;div&#39;);\n      elementType.className = &#39;a11y-element-type&#39;;\n      elementType.textContent = item.type ?\n      item.type.charAt(0).toUpperCase() + item.type.slice(1) :\n      &#39;Element&#39;;\n      element.appendChild(elementType);\n\n      const announcement = document.createElement(&#39;div&#39;);\n      announcement.className = &#39;a11y-element-announcement&#39;;\n      announcement.textContent = item.announcement;\n      element.appendChild(announcement);\n\n      resultContent.appendChild(element);\n\n      // Add warnings to the warnings container\n      if (item.warnings &amp;&amp; item.warnings.length &gt; 0) {\n        item.warnings.forEach(warning =&gt; {\n          this.showWarning(warning, item.type || &#39;element&#39;);\n        });\n      }\n    });\n\n    resultElement.appendChild(resultContent);\n    this.outputContainer.appendChild(resultElement);\n  }\n\n  showWarning(message, context = &#39;&#39;) {\n    const warning = document.createElement(&#39;div&#39;);\n    warning.className = &#39;a11y-warning&#39;;\n    warning.textContent = context ? `${context}: ${message}` : message;\n    this.warningsContainer.appendChild(warning);\n  }\n\n  startSpeaking() {\n    if (this.isSpeaking) return;\n\n    this.isSpeaking = true;\n    this.currentSpeakingIndex = 0;\n    this.playButton.disabled = true;\n    this.stopButton.disabled = false;\n\n    this.speakCurrent();\n  }\n\n  speakCurrent() {\n    if (!this.isSpeaking ||\n    this.currentSpeakingIndex &gt;= this.accessibilityOutput.length) {\n      this.stopSpeaking();\n      return;\n    }\n\n    // Highlight current element\n    const elements = this.shadowRoot.querySelectorAll(&#39;.a11y-element&#39;);\n    elements.forEach(el =&gt; el.classList.remove(&#39;speaking&#39;));\n\n    const currentElement = this.shadowRoot.querySelector(`.a11y-element[data-index=&quot;${this.currentSpeakingIndex}&quot;]`);\n    if (currentElement) {\n      currentElement.classList.add(&#39;speaking&#39;);\n      currentElement.scrollIntoView({ behavior: &#39;smooth&#39;, block: &#39;center&#39; });\n    }\n\n    // Get current announcement\n    const item = this.accessibilityOutput[this.currentSpeakingIndex];\n    const text = item.announcement;\n\n    // Use speech synthesis to speak the text\n    const utterance = new SpeechSynthesisUtterance(text);\n    utterance.onend = () =&gt; {\n      // Move to next item after speaking completes\n      this.currentSpeakingIndex++;\n      setTimeout(() =&gt; this.speakCurrent(), 500); // Small delay between items\n    };\n\n    this.speechSynthesis.speak(utterance);\n  }\n\n  stopSpeaking() {\n    this.isSpeaking = false;\n    this.speechSynthesis.cancel();\n    this.playButton.disabled = false;\n    this.stopButton.disabled = true;\n\n    // Remove highlighting\n    const elements = this.shadowRoot.querySelectorAll(&#39;.a11y-element&#39;);\n    elements.forEach(el =&gt; el.classList.remove(&#39;speaking&#39;));\n  }\n\n  insertNonSemanticExample() {\n    this.htmlInput.value = this.getNonSemanticHtml();\n    // Auto-process the example\n    this.processHtml();\n  }\n\n  insertSemanticExample() {\n    this.htmlInput.value = this.getSemanticHtml();\n    // Auto-process the example\n    this.processHtml();\n  }\n\n  getNonSemanticHtml() {\n    return `&lt;div&gt;\n  &lt;div style=&quot;font-size: 24px; font-weight: bold;&quot;&gt;My Online Store&lt;/div&gt;\n  \n  &lt;div style=&quot;background-color: #f0f0f0; padding: 10px;&quot;&gt;\n    &lt;div style=&quot;font-weight: bold;&quot;&gt;Products&lt;/div&gt;\n    &lt;div&gt;Home&lt;/div&gt;\n    &lt;div&gt;About&lt;/div&gt;\n    &lt;div&gt;Contact&lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div&gt;\n    &lt;div style=&quot;font-size: 20px; font-weight: bold;&quot;&gt;Featured Products&lt;/div&gt;\n    \n    &lt;div style=&quot;display: flex; flex-wrap: wrap;&quot;&gt;\n      &lt;div style=&quot;border: 1px solid #ddd; margin: 10px; padding: 15px; width: 200px;&quot;&gt;\n        &lt;img src=&quot;https://via.placeholder.com/150&quot; alt=&quot;&quot;&gt;\n        &lt;div style=&quot;font-weight: bold;&quot;&gt;Product 1&lt;/div&gt;\n        &lt;div&gt;$19.99&lt;/div&gt;\n        &lt;div style=&quot;color: #666;&quot;&gt;★★★★☆ (4.2/5)&lt;/div&gt;\n        &lt;div style=&quot;background-color: #0066cc; color: white; padding: 5px; text-align: center; cursor: pointer;&quot;&gt;Add to Cart&lt;/div&gt;\n      &lt;/div&gt;\n      \n      &lt;div style=&quot;border: 1px solid #ddd; margin: 10px; padding: 15px; width: 200px;&quot;&gt;\n        &lt;img src=&quot;https://via.placeholder.com/150&quot;&gt;\n        &lt;div style=&quot;font-weight: bold;&quot;&gt;Product 2&lt;/div&gt;\n        &lt;div&gt;$24.99&lt;/div&gt;\n        &lt;div style=&quot;color: #666;&quot;&gt;★★★★★ (4.9/5)&lt;/div&gt;\n        &lt;div style=&quot;background-color: #0066cc; color: white; padding: 5px; text-align: center; cursor: pointer;&quot;&gt;Add to Cart&lt;/div&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/div&gt;\n  \n  &lt;div style=&quot;margin-top: 20px; background-color: #333; color: white; padding: 10px; text-align: center;&quot;&gt;\n    © 2023 My Online Store. All rights reserved.\n  &lt;/div&gt;\n&lt;/div&gt;`;\n  }\n\n  getSemanticHtml() {\n    return `&lt;header&gt;\n  &lt;h1&gt;My Online Store&lt;/h1&gt;\n  \n  &lt;nav aria-label=&quot;Main Navigation&quot;&gt;\n    &lt;ul&gt;\n      &lt;li&gt;&lt;a href=&quot;#products&quot; aria-current=&quot;page&quot;&gt;Products&lt;/a&gt;&lt;/li&gt;\n      &lt;li&gt;&lt;a href=&quot;/&quot;&gt;Home&lt;/a&gt;&lt;/li&gt;\n      &lt;li&gt;&lt;a href=&quot;/about&quot;&gt;About&lt;/a&gt;&lt;/li&gt;\n      &lt;li&gt;&lt;a href=&quot;/contact&quot;&gt;Contact&lt;/a&gt;&lt;/li&gt;\n    &lt;/ul&gt;\n  &lt;/nav&gt;\n&lt;/header&gt;\n\n&lt;main&gt;\n  &lt;section aria-labelledby=&quot;featured-heading&quot;&gt;\n    &lt;h2 id=&quot;featured-heading&quot;&gt;Featured Products&lt;/h2&gt;\n    \n    &lt;div class=&quot;product-grid&quot;&gt;\n      &lt;article class=&quot;product&quot;&gt;\n        &lt;img src=&quot;https://via.placeholder.com/150&quot; alt=&quot;Featured product 1 thumbnail&quot; width=&quot;150&quot; height=&quot;150&quot;&gt;\n        &lt;h3&gt;Product 1&lt;/h3&gt;\n        &lt;p class=&quot;price&quot;&gt;$19.99&lt;/p&gt;\n        &lt;div class=&quot;ratings&quot; aria-label=&quot;Rated 4.2 out of 5 stars&quot;&gt;★★★★☆ (4.2/5)&lt;/div&gt;\n        &lt;button type=&quot;button&quot; aria-label=&quot;Add Product 1 to cart&quot;&gt;Add to Cart&lt;/button&gt;\n      &lt;/article&gt;\n      \n      &lt;article class=&quot;product&quot;&gt;\n        &lt;img src=&quot;https://via.placeholder.com/150&quot; alt=&quot;Featured product 2 thumbnail&quot; width=&quot;150&quot; height=&quot;150&quot;&gt;\n        &lt;h3&gt;Product 2&lt;/h3&gt;\n        &lt;p class=&quot;price&quot;&gt;$24.99&lt;/p&gt;\n        &lt;div class=&quot;ratings&quot; aria-label=&quot;Rated 4.9 out of 5 stars&quot;&gt;★★★★★ (4.9/5)&lt;/div&gt;\n        &lt;button type=&quot;button&quot; aria-label=&quot;Add Product 2 to cart&quot;&gt;Add to Cart&lt;/button&gt;\n      &lt;/article&gt;\n    &lt;/div&gt;\n  &lt;/section&gt;\n&lt;/main&gt;\n\n&lt;footer&gt;\n  &lt;p&gt;© 2023 My Online Store. All rights reserved.&lt;/p&gt;\n&lt;/footer&gt;`;\n  }}\n\n\n// Register the custom element\ncustomElements.define(&#39;a11y-tester&#39;, AccessibilityTester);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<h3 id=\"4-testing-with-nvda\">4. Testing with NVDA<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#4-testing-with-nvda\" aria-label=\"Link to this section\">#</a></h3>\n<p>NVDA (NonVisual Desktop Access) is a free screen reader for Windows. Download it from <a href=\"https://www.nvaccess.org/\">nvaccess.org</a>. After installation, you'll hear a startup sound and voice feedback will begin. Use the NVDA key (usually Insert) combined with other keys to give it commands. Press NVDA+Space to switch between browse mode (reading the page) and focus mode (typing into a control).</p>\n<p>Pay attention to how NVDA announces your interface elements. Ensure all interactive elements are properly labeled and keyboard-accessible. Test different scenarios: form completion, menu navigation, and custom component interaction. In browse mode, single keys jump around the page: <code>H</code> to the next heading and <code>D</code> to the next landmark. Listen for proper announcements of alerts, verify that dynamic content changes are announced, and check that error messages are accessible.</p>\n<p>This is the closest you can get to Sarah's experience without her: turn off your monitor, or look away, and try to finish the component's task by ear and keyboard.</p>\n<h2 id=\"common-accessibility-pitfalls-in-web-components\">Common Accessibility Pitfalls in Web Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#common-accessibility-pitfalls-in-web-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>Even with thorough testing, certain accessibility issues frequently emerge in Web Component development. By understanding these common pitfalls, you can address them proactively in your implementation.</p>\n<h3 id=\"1-shadow-dom-isolation-issues\">1. Shadow DOM Isolation Issues<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#1-shadow-dom-isolation-issues\" aria-label=\"Link to this section\">#</a></h3>\n<p><strong>Problem:</strong> ARIA relationships that cross shadow boundaries don't work as expected.</p>\n<p><strong>Solution:</strong> Keep related elements within the same shadow root or use alternative techniques like <code>aria-live</code> regions for updates.</p>\n<h3 id=\"2-forgetting-about-non-mouse-users\">2. Forgetting About Non-Mouse Users<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#2-forgetting-about-non-mouse-users\" aria-label=\"Link to this section\">#</a></h3>\n<p><strong>Problem:</strong> Components only work with mouse events (<code>click</code>, <code>hover</code>).</p>\n<p><strong>Solution:</strong> Start from a real <code>&lt;button&gt;</code>. It is focusable and fires <code>click</code> for Enter and Space, so one handler covers mouse, keyboard and touch. If you truly cannot use a button, you have to rebuild all of that by hand.</p>\n<pre><code class=\"language-javascript\">// Bad example - a &lt;div&gt; only reacts to the mouse.\n// It cannot be reached with Tab, and Enter or Space do nothing.\nconst fake = document.createElement('div');\nfake.textContent = 'Save';\nfake.addEventListener('click', handleAction);\n\n// Good example - a &lt;button&gt; needs no extra code\nconst button = document.createElement('button');\nbutton.textContent = 'Save';\nbutton.addEventListener('click', handleAction);\n\n// Last resort - keep the &lt;div&gt;, but add everything a button gives you\nfake.setAttribute('role', 'button');\nfake.setAttribute('tabindex', '0');\nfake.addEventListener('keydown', e =&gt; {\n  if (e.key === 'Enter' || e.key === ' ') {\n    e.preventDefault();\n    handleAction();\n  }\n});\n</code></pre>\n<p>Now try it yourself. The starter below is a \"button\" built from a <code>&lt;div&gt;</code>, with the extra code it needs.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Challenge: delete code by using a real button</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div id=&quot;save&quot; role=&quot;button&quot; tabindex=&quot;0&quot;&gt;Save&lt;/div&gt;\n&lt;p id=&quot;status&quot;&gt;Saved 0 times.&lt;/p&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\n#save, button { display: inline-block; padding: 8px 16px; border: 2px solid #333; border-radius: 4px; cursor: pointer; }\n#save:focus-visible, button:focus-visible { outline: 3px solid blue; outline-offset: 2px; }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">let count = 0;\nconst statusEl = document.getElementById(&#39;status&#39;);\n\nfunction save() {\n  count++;\n  statusEl.textContent = `Saved ${count} times.`;\n}\n\nconst control = document.getElementById(&#39;save&#39;);\ncontrol.addEventListener(&#39;click&#39;, save);\n\n// TODO: once #save is a real &lt;button&gt;, delete this whole block\ncontrol.addEventListener(&#39;keydown&#39;, (e) =&gt; {\n  if (e.key === &#39;Enter&#39; || e.key === &#39; &#39;) {\n    e.preventDefault();\n    save();\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<p>Press Tab, then Enter and Space, and watch the counter. Then: in the HTML tab, replace the <code>&lt;div id=\"save\" role=\"button\" tabindex=\"0\"&gt;Save&lt;/div&gt;</code> with <code>&lt;button id=\"save\"&gt;Save&lt;/button&gt;</code>, and in the JavaScript tab delete the <code>keydown</code> block. Count the lines you deleted. The expected result is the same behavior (Tab reaches it, Enter and Space each add one to the counter, a click adds one) with the whole 6-line <code>keydown</code> block gone, plus the <code>role</code> and <code>tabindex</code> attributes.</p>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Nothing visibly breaks if you leave the <code>keydown</code> block in place, because its <code>preventDefault()</code> stops the button's own click for those keys, so the count still goes up by one. That is the lesson: the block is now dead weight. To see why the <code>&lt;div&gt;</code> needed it, change the HTML to a plain <code>&lt;div id=\"save\"&gt;Save&lt;/div&gt;</code> and press Tab: focus never lands on it, and Enter does nothing.</p>\n</aside>\n<h3 id=\"3-poor-focus-management\">3. Poor Focus Management<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#3-poor-focus-management\" aria-label=\"Link to this section\">#</a></h3>\n<p><strong>Problem:</strong> Focus gets lost during dynamic updates or when elements are removed.</p>\n<p><strong>Solution:</strong> Always manage focus explicitly, especially when showing/hiding content.</p>\n<h3 id=\"4-insufficient-color-contrast\">4. Insufficient Color Contrast<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#4-insufficient-color-contrast\" aria-label=\"Link to this section\">#</a></h3>\n<p><strong>Problem:</strong> Text or UI controls don't have sufficient contrast with their backgrounds.</p>\n<p><strong>Solution:</strong> Follow WCAG guidelines for contrast ratios (4.5:1 for normal text, 3:1 for large text).</p>\n<p>Now that we've identified potential problems, let's consolidate what we've learned into actionable best practices that will guide your development process from ideation through implementation.</p>\n<h2 id=\"best-practices\">Best Practices<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#best-practices\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><strong>Use Native Elements When Possible:</strong> Use native HTML elements with inherent accessibility support.</li>\n<li><strong>Provide Accessible Names:</strong> Ensure all interactive elements have accessible names using <code>aria-label</code>, <code>aria-labelledby</code>, or semantic HTML.</li>\n<li><strong>Ensure Focus Visibility:</strong> Maintain visible focus indicators for keyboard and assistive technology users.</li>\n<li><strong>Handle Dynamic Content Carefully:</strong> When updating content, manage focus appropriately and announce changes.</li>\n<li><strong>Test Regularly:</strong> Incorporate accessibility testing into your development process.</li>\n<li><strong>Document Accessibility Features:</strong> Include accessibility information in your component documentation.</li>\n</ul>\n<p>These best practices come together most effectively when applied to real-world scenarios. Let us look at a larger example that brings several accessibility features together into a practical, commonly-used component:</p>\n<h2 id=\"real-world-example-an-accessible-search-component\">Real-World Example: An Accessible Search Component<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#real-world-example-an-accessible-search-component\" aria-label=\"Link to this section\">#</a></h2>\n<p>Let's combine everything we've learned into a practical example:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Accessible Search Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;accessible-search&gt;&lt;/accessible-search&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Simple Dark Mode CSS */\n\n/* Default light mode */\n:root {\n\t--bg-color: #ffffff;\n\t--text-color: #333333;\n}\n\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t}\n}\n\n/* Basic styling */\nbody {\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, &quot;Segoe UI&quot;, Roboto,\n\t\tsans-serif;\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 0;\n\ttransition: background-color 0.3s ease, color 0.3s ease;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: center;\n\tjustify-content: center;\n\tmin-height: 100vh;\n\ttext-align: center;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class AccessibleSearch extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; });\n    this.results = [];\n    this.render();\n  }\n\n  connectedCallback() {\n    // Get DOM elements\n    const container = this.shadowRoot.querySelector(&#39;.search-container&#39;);\n    const input = this.shadowRoot.querySelector(&#39;input&#39;);\n    const button = this.shadowRoot.querySelector(&#39;button&#39;);\n    const results = this.shadowRoot.querySelector(&#39;.results&#39;);\n    const themeToggle = this.shadowRoot.querySelector(&#39;#theme-toggle&#39;);\n\n    // Example data - in a real component, this might come from an API\n    const sampleData = [\n    &#39;JavaScript&#39;, &#39;TypeScript&#39;, &#39;HTML&#39;, &#39;CSS&#39;, &#39;React&#39;,\n    &#39;Angular&#39;, &#39;Vue&#39;, &#39;Svelte&#39;, &#39;Web Components&#39;, &#39;Accessibility&#39;];\n\n\n    // Check for system preference initially\n    const prefersDarkMode = window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).matches;\n    if (prefersDarkMode) {\n      container.classList.add(&#39;dark-theme&#39;);\n    }\n\n    // Listen for system preference changes\n    window.matchMedia(&#39;(prefers-color-scheme: dark)&#39;).addEventListener(&#39;change&#39;, e =&gt; {\n      if (e.matches) {\n        container.classList.add(&#39;dark-theme&#39;);\n      } else {\n        container.classList.remove(&#39;dark-theme&#39;);\n      }\n    });\n\n    // Theme toggle functionality\n    themeToggle.addEventListener(&#39;click&#39;, () =&gt; {\n      container.classList.toggle(&#39;dark-theme&#39;);\n\n      // Announce theme change to screen readers\n      const isDarkTheme = container.classList.contains(&#39;dark-theme&#39;);\n      const announcement = this.shadowRoot.querySelector(&#39;.theme-announcement&#39;);\n      announcement.textContent = `Theme changed to ${isDarkTheme ? &#39;dark&#39; : &#39;light&#39;} mode`;\n    });\n\n    // Search input handling\n    input.addEventListener(&#39;input&#39;, () =&gt; {\n      const query = input.value.toLowerCase();\n      if (query.length &lt; 2) {\n        this.results = [];\n      } else {\n        this.results = sampleData.filter((item) =&gt;\n        item.toLowerCase().includes(query));\n\n      }\n      this.updateResults();\n    });\n\n    // Search button click\n    button.addEventListener(&#39;click&#39;, () =&gt; {\n      this.performSearch();\n    });\n\n    // Handle keyboard input\n    input.addEventListener(&#39;keydown&#39;, e =&gt; {\n      if (e.key === &#39;Enter&#39;) {\n        e.preventDefault();\n        this.performSearch();\n      }\n\n      // Allow keyboard navigation through results\n      if (e.key === &#39;ArrowDown&#39; &amp;&amp; this.results.length &gt; 0) {\n        e.preventDefault();\n        this.shadowRoot.querySelector(&#39;.result-item&#39;).focus();\n      }\n    });\n\n    // Delegate event listener for result items\n    results.addEventListener(&#39;keydown&#39;, e =&gt; {\n      if (!e.target.classList.contains(&#39;result-item&#39;)) return;\n\n      const resultItems = Array.from(this.shadowRoot.querySelectorAll(&#39;.result-item&#39;));\n      const currentIndex = resultItems.indexOf(e.target);\n\n      if (e.key === &#39;ArrowDown&#39; &amp;&amp; currentIndex &lt; resultItems.length - 1) {\n        e.preventDefault();\n        resultItems[currentIndex + 1].focus();\n      }\n\n      if (e.key === &#39;ArrowUp&#39;) {\n        e.preventDefault();\n        if (currentIndex === 0) {\n          input.focus();\n        } else {\n          resultItems[currentIndex - 1].focus();\n        }\n      }\n\n      if (e.key === &#39;Enter&#39;) {\n        e.preventDefault();\n        this.selectResult(e.target.textContent);\n      }\n\n      if (e.key === &#39;Escape&#39;) {\n        e.preventDefault();\n        this.results = [];\n        this.updateResults();\n        input.focus();\n      }\n    });\n\n    // Handle result item clicks\n    results.addEventListener(&#39;click&#39;, e =&gt; {\n      if (e.target.classList.contains(&#39;result-item&#39;)) {\n        this.selectResult(e.target.textContent);\n      }\n    });\n  }\n\n  performSearch() {\n    const announcement = this.shadowRoot.querySelector(&#39;.search-announcement&#39;);\n    const resultCount = this.results.length;\n\n    announcement.textContent = `Found ${resultCount} result${resultCount === 1 ? &#39;&#39; : &#39;s&#39;}`;\n  }\n\n  selectResult(text) {\n    const input = this.shadowRoot.querySelector(&#39;input&#39;);\n    input.value = text;\n    this.results = [];\n    this.updateResults();\n    input.focus();\n\n    // Announce selection to screen readers\n    const announcement = this.shadowRoot.querySelector(&#39;.search-announcement&#39;);\n    announcement.textContent = `Selected ${text}`;\n  }\n\n  updateResults() {\n    const resultsContainer = this.shadowRoot.querySelector(&#39;.results&#39;);\n\n    if (this.results.length === 0) {\n      resultsContainer.innerHTML = &#39;&#39;;\n      resultsContainer.setAttribute(&#39;aria-hidden&#39;, &#39;true&#39;);\n      return;\n    }\n\n    resultsContainer.removeAttribute(&#39;aria-hidden&#39;);\n    resultsContainer.innerHTML = this.results.map((result) =&gt;\n    `&lt;button class=&quot;result-item&quot; tabindex=&quot;0&quot;&gt;${result}&lt;/button&gt;`).\n    join(&#39;&#39;);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        /* Base styles */\n        * {\n          box-sizing: border-box;\n          margin: 0;\n          padding: 0;\n        }\n        \n        /* Container styles */\n        .search-container {\n          position: relative;\n          width: 100%;\n          max-width: 500px;\n          font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n          padding: 20px;\n          border-radius: 8px;\n          background-color: #ffffff;\n          color: #333333;\n          transition: all 0.3s ease;\n          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n        }\n        \n        /* Dark theme */\n        .search-container.dark-theme {\n          background-color: #1e1e1e;\n          color: #e0e0e0;\n          box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n        }\n        \n        /* Header with search label and theme toggle */\n        .search-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          margin-bottom: 12px;\n        }\n        \n        label {\n          font-size: 16px;\n          font-weight: 500;\n        }\n        \n        .dark-theme label {\n          color: #f0f0f0;\n        }\n        \n        /* Search input container */\n        .search-input {\n          display: flex;\n          width: 100%;\n          margin-bottom: 8px;\n        }\n        \n        input {\n          flex: 1;\n          padding: 10px 12px;\n          border: 2px solid #ccc;\n          border-radius: 4px 0 0 4px;\n          font-size: 16px;\n          background-color: #ffffff;\n          color: #333333;\n          transition: all 0.2s ease;\n        }\n        \n        .dark-theme input {\n          background-color: #2d2d2d;\n          border-color: #444;\n          color: #e0e0e0;\n        }\n        \n        input:focus {\n          outline: none;\n          border-color: #4a90e2;\n          box-shadow: 0 0 0 1px #4a90e2;\n        }\n        \n        .dark-theme input:focus {\n          border-color: #5a9de4;\n          box-shadow: 0 0 0 1px #5a9de4;\n        }\n        \n        /* Button styles */\n        button {\n          padding: 10px 16px;\n          font-size: 16px;\n          cursor: pointer;\n          background-color: #4a90e2;\n          color: white;\n          border: none;\n          transition: all 0.2s ease;\n        }\n        \n        .dark-theme button {\n          background-color: #3a7bc2;\n        }\n        \n        button:hover {\n          background-color: #3a7bc2;\n        }\n        \n        .dark-theme button:hover {\n          background-color: #2a6cb2;\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #90c2ff;\n          outline-offset: 2px;\n          position: relative;\n          z-index: 1;\n        }\n        \n        .dark-theme button:focus-visible {\n          outline-color: #5a9de4;\n        }\n        \n        /* Search button */\n        #search-button {\n          border-radius: 0 4px 4px 0;\n        }\n        \n        /* Theme toggle button */\n        #theme-toggle {\n          background-color: #6c757d;\n          border-radius: 4px;\n          margin-left: 10px;\n          padding: 8px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n        \n        .dark-theme #theme-toggle {\n          background-color: #495057;\n        }\n        \n        #theme-toggle:hover {\n          background-color: #5a6268;\n        }\n        \n        .dark-theme #theme-toggle:hover {\n          background-color: #3d4348;\n        }\n        \n        /* Theme icon */\n        .theme-icon {\n          width: 20px;\n          height: 20px;\n          fill: white;\n        }\n        \n        /* Results container */\n        .results {\n          position: absolute;\n          top: 100%;\n          left: 20px;\n          right: 20px;\n          z-index: 10;\n          max-height: 300px;\n          overflow-y: auto;\n          background: white;\n          border: 1px solid #ccc;\n          border-radius: 0 0 4px 4px;\n          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n          margin-top: -8px;\n        }\n        \n        .dark-theme .results {\n          background-color: #2d2d2d;\n          border-color: #444;\n          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);\n        }\n        \n        /* Result items */\n        .result-item {\n          display: block;\n          width: 100%;\n          text-align: left;\n          padding: 10px 16px;\n          background: none;\n          border: none;\n          border-bottom: 1px solid #eee;\n          cursor: pointer;\n          color: #333;\n          transition: background-color 0.2s;\n        }\n        \n        .dark-theme .result-item {\n          border-bottom-color: #444;\n          color: #e0e0e0;\n        }\n        \n        .result-item:last-child {\n          border-bottom: none;\n        }\n        \n        .result-item:hover, .result-item:focus {\n          background-color: #f5f5f5;\n        }\n        \n        .dark-theme .result-item:hover, .dark-theme .result-item:focus {\n          background-color: #3d3d3d;\n        }\n        \n        /* Screen reader only elements */\n        .sr-only {\n          position: absolute;\n          width: 1px;\n          height: 1px;\n          padding: 0;\n          margin: -1px;\n          overflow: hidden;\n          clip: rect(0, 0, 0, 0);\n          white-space: nowrap;\n          border-width: 0;\n        }\n        \n        /* For high contrast mode support */\n        @media (forced-colors: active) {\n          input:focus, button:focus-visible {\n            outline: 3px solid CanvasText;\n          }\n          \n          .result-item:focus {\n            outline: 3px solid CanvasText;\n            outline-offset: -3px;\n          }\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;search-container&quot;&gt;\n        &lt;div class=&quot;search-header&quot;&gt;\n          &lt;label id=&quot;search-label&quot; for=&quot;search-input&quot;&gt;Search for web technologies&lt;/label&gt;\n          &lt;button id=&quot;theme-toggle&quot; aria-label=&quot;Toggle dark theme&quot;&gt;\n            &lt;svg class=&quot;theme-icon&quot; viewBox=&quot;0 0 24 24&quot; aria-hidden=&quot;true&quot;&gt;\n              &lt;path d=&quot;M12,22 C17.5228475,22 22,17.5228475 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,17.5228475 6.4771525,22 12,22 Z M12,20 L12,4 C16.418278,4 20,7.581722 20,12 C20,16.418278 16.418278,20 12,20 Z&quot;&gt;&lt;/path&gt;\n            &lt;/svg&gt;\n          &lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;search-input&quot;&gt;\n          &lt;input \n            id=&quot;search-input&quot; \n            type=&quot;text&quot; \n            aria-labelledby=&quot;search-label&quot; \n            aria-autocomplete=&quot;list&quot;\n            aria-controls=&quot;search-results&quot;\n            aria-expanded=&quot;false&quot;\n            role=&quot;combobox&quot;\n            autocomplete=&quot;off&quot;\n            placeholder=&quot;Type to search...&quot;\n          /&gt;\n          &lt;button id=&quot;search-button&quot; aria-label=&quot;Search&quot;&gt;\n            Search\n          &lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div \n          id=&quot;search-results&quot; \n          class=&quot;results&quot; \n          role=&quot;listbox&quot; \n          aria-label=&quot;Search suggestions&quot;\n          aria-hidden=&quot;true&quot;\n        &gt;&lt;/div&gt;\n        \n        &lt;!-- Live regions for screen readers --&gt;\n        &lt;div class=&quot;search-announcement sr-only&quot; aria-live=&quot;polite&quot;&gt;&lt;/div&gt;\n        &lt;div class=&quot;theme-announcement sr-only&quot; aria-live=&quot;polite&quot;&gt;&lt;/div&gt;\n        \n        &lt;div id=&quot;instructions&quot; class=&quot;sr-only&quot;&gt;\n          Use up and down arrow keys to navigate results. Press Enter to select a result. Press Escape to clear results.\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }}\n\n\ncustomElements.define(&#39;accessible-search&#39;, AccessibleSearch);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/\">the original article</a>.</p></figure>\n<details>\n<summary>Accessible search JavaScript · 222 lines</summary>\n<pre><code class=\"language-javascript\">class AccessibleSearch extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: 'open' });\n    this.results = [];\n    this.render();\n  }\n\n  connectedCallback() {\n    const input = this.shadowRoot.querySelector('input');\n    const button = this.shadowRoot.querySelector('button');\n    const results = this.shadowRoot.querySelector('.results');\n\n    // Example data - in a real component, this might come from an API\n    const sampleData = [\n      'JavaScript', 'TypeScript', 'HTML', 'CSS', 'React', \n      'Angular', 'Vue', 'Svelte', 'Web Components', 'Accessibility'\n    ];\n\n    input.addEventListener('input', () =&gt; {\n      const query = input.value.toLowerCase();\n      if (query.length &lt; 2) {\n        this.results = [];\n      } else {\n        this.results = sampleData.filter(item =&gt; \n          item.toLowerCase().includes(query)\n        );\n      }\n      this.updateResults();\n    });\n\n    button.addEventListener('click', () =&gt; {\n      this.performSearch();\n    });\n\n    input.addEventListener('keydown', e =&gt; {\n      if (e.key === 'Enter') {\n        e.preventDefault();\n        this.performSearch();\n      }\n\n      // Allow keyboard navigation through results\n      if (e.key === 'ArrowDown' &amp;&amp; this.results.length &gt; 0) {\n        e.preventDefault();\n        this.shadowRoot.querySelector('.result-item').focus();\n      }\n    });\n\n    // Delegate event listener for result items\n    results.addEventListener('keydown', e =&gt; {\n      if (!e.target.classList.contains('result-item')) return;\n\n      const resultItems = Array.from(this.shadowRoot.querySelectorAll('.result-item'));\n      const currentIndex = resultItems.indexOf(e.target);\n\n      if (e.key === 'ArrowDown' &amp;&amp; currentIndex &lt; resultItems.length - 1) {\n        e.preventDefault();\n        resultItems[currentIndex + 1].focus();\n      }\n\n      if (e.key === 'ArrowUp') {\n        e.preventDefault();\n        if (currentIndex === 0) {\n          input.focus();\n        } else {\n          resultItems[currentIndex - 1].focus();\n        }\n      }\n\n      if (e.key === 'Enter') {\n        e.preventDefault();\n        this.selectResult(e.target.textContent);\n      }\n    });\n  }\n\n  performSearch() {\n    const announcement = this.shadowRoot.querySelector('.screen-reader-announcement');\n    const resultCount = this.results.length;\n\n    announcement.textContent = `Found ${resultCount} result${resultCount === 1 ? '' : 's'}`;\n  }\n\n  selectResult(text) {\n    const input = this.shadowRoot.querySelector('input');\n    input.value = text;\n    this.results = [];\n    this.updateResults();\n    input.focus();\n\n    // Announce selection to screen readers\n    const announcement = this.shadowRoot.querySelector('.screen-reader-announcement');\n    announcement.textContent = `Selected ${text}`;\n  }\n\n  updateResults() {\n    const resultsContainer = this.shadowRoot.querySelector('.results');\n\n    if (this.results.length === 0) {\n      resultsContainer.innerHTML = '';\n      resultsContainer.setAttribute('aria-hidden', 'true');\n      return;\n    }\n\n    resultsContainer.removeAttribute('aria-hidden');\n    resultsContainer.innerHTML = this.results.map(result =&gt; \n      `&lt;button class=\"result-item\" tabindex=\"0\"&gt;${result}&lt;/button&gt;`\n    ).join('');\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        .search-container {\n          position: relative;\n          width: 300px;\n        }\n\n        .search-input {\n          display: flex;\n        }\n\n        input {\n          flex: 1;\n          padding: 8px;\n          border: 2px solid #ccc;\n          border-radius: 4px 0 0 4px;\n          font-size: 16px;\n        }\n\n        input:focus {\n          outline: none;\n          border-color: blue;\n        }\n\n        button {\n          padding: 8px 16px;\n          background-color: #4CAF50;\n          color: white;\n          border: none;\n          border-radius: 0 4px 4px 0;\n          cursor: pointer;\n        }\n\n        button:focus-visible {\n          outline: 3px solid blue;\n          outline-offset: 2px;\n        }\n\n        .results {\n          position: absolute;\n          top: 100%;\n          left: 0;\n          right: 0;\n          z-index: 10;\n          background: white;\n          border: 1px solid #ccc;\n          border-top: none;\n          max-height: 200px;\n          overflow-y: auto;\n        }\n\n        .result-item {\n          display: block;\n          width: 100%;\n          text-align: left;\n          padding: 8px;\n          background: none;\n          border: none;\n          border-bottom: 1px solid #eee;\n          cursor: pointer;\n        }\n\n        .result-item:hover, .result-item:focus {\n          background-color: #f1f1f1;\n        }\n\n        .screen-reader-only {\n          position: absolute;\n          width: 1px;\n          height: 1px;\n          padding: 0;\n          margin: -1px;\n          overflow: hidden;\n          clip: rect(0, 0, 0, 0);\n          white-space: nowrap;\n          border-width: 0;\n        }\n      &lt;/style&gt;\n\n      &lt;div class=\"search-container\"&gt;\n        &lt;label id=\"search-label\" for=\"search-input\"&gt;Search&lt;/label&gt;\n        &lt;div class=\"search-input\"&gt;\n          &lt;input \n            id=\"search-input\" \n            type=\"text\" \n            aria-labelledby=\"search-label\" \n            aria-autocomplete=\"list\"\n            aria-controls=\"search-results\"\n            autocomplete=\"off\"\n          /&gt;\n          &lt;button aria-label=\"Search\"&gt;Go&lt;/button&gt;\n        &lt;/div&gt;\n\n        &lt;div \n          id=\"search-results\" \n          class=\"results\" \n          role=\"listbox\" \n          aria-label=\"Search suggestions\"\n          aria-hidden=\"true\"\n        &gt;&lt;/div&gt;\n\n        &lt;div \n          class=\"screen-reader-announcement screen-reader-only\" \n          aria-live=\"polite\"\n        &gt;&lt;/div&gt;\n      &lt;/div&gt;\n    `;\n  }\n}\n\ncustomElements.define('accessible-search', AccessibleSearch);\n</code></pre>\n</details>\n<p><strong>Key accessibility features:</strong></p>\n<ul>\n<li>Proper labeling of all elements</li>\n<li>Keyboard navigation through results with arrow keys</li>\n<li>Proper ARIA roles and relationships</li>\n<li>Live region announcements for search results and selections</li>\n<li>Full keyboard functionality without requiring a mouse</li>\n</ul>\n<p>As this search component demonstrates, accessible Web Components require attention to multiple factors—from semantic structure to keyboard interactions and dynamic announcements. By applying these techniques to your own components, you'll create experiences that work for everyone.</p>\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>Creating accessible Web Components isn't just about compliance—it's about creating a better web for everyone. By incorporating these practices from the start, you'll build components that more people can use.</p>\n<p>Remember these key principles:</p>\n<ol>\n<li>Use semantic HTML whenever possible</li>\n<li>Implement proper keyboard support</li>\n<li>Manage focus explicitly</li>\n<li>Announce dynamic changes with ARIA live regions</li>\n<li>Test thoroughly with both automated and manual methods</li>\n</ol>\n<p>By building accessibility into your Web Components from the ground up, you're not just helping users with disabilities—you're creating a better experience for everyone.</p>\n<hr />\n<h2 id=\"resources\">Resources<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#resources\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li><a href=\"https://www.w3.org/WAI/\">Web Accessibility Initiative (WAI)</a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/Web_Components\">MDN Web Components Guide</a></li>\n<li><a href=\"https://www.w3.org/WAI/ARIA/apg/\">ARIA Authoring Practices Guide</a></li>\n<li><a href=\"https://github.com/mnichols08/WebComponentAccesibility/\">GitHub repository containing these examples</a></li>\n<li><a href=\"https://mnichols08.github.io/WebComponentAccesibility/\">Demo Page from the repository</a></li>\n</ul>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/accessibility-in-web-components/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Accessibility in Web Components\nCanonical: https://mnixblog.netlify.app/accessibility-in-web-components/\nEnsuring Inclusivity in Custom Elements\nIntroduction\nMeet Sarah, a web user who navigates the internet using a screen reader. When she encounters custom elements on websites, her experience isn't determined by visual design—it's determined by whether developers prioritized accessibility. When developers overlook the needs of millions of users with disabilities, they transform bridges into barriers, creating frustration instead of inclusive solutions.\nA screen reader reads a page aloud from its structure, not from how it looks. A custom element that only looks like a button is silent or confusing to Sarah, and often unreachable for anyone who cannot use a mouse. This post builds custom elements that say what they are, work from the keyboard, and announce changes.\nQuick recap if you are joining here: a custom element is a class that extends HTMLElement, registered with customElements.define('my-tag', MyClass), and Shadow DOM gives it a private inside that the page's CSS and scripts cannot reach by accident. ARIA (Accessible Rich Internet Applications) is a set of HTML attributes, such as role, aria-pressed and aria-label, that tell assistive technology what a control is and what state it is in. ARIA only changes what is announced; it adds no behavior. Whenever a native element does the job (a real <button>, for instance), use it and skip the ARIA.\nA reusable element is only reusable if everyone can use it. This guide covers semantic HTML, ARIA roles, keyboard navigation and testing for custom elements, with interactive examples you can change and run.\nWith these fundamentals in mind, let's see accessibility in action.\nThe Accessibility Difference\nThe following examples demonstrate how seemingly small details create dramatically different user experiences:\nBefore diving into details, let's visualize the difference accessibility makes:\nHTML\n<h3>Non-accessible Toggle</h3>\n<div class=\"toggle-button\" onclick=\"this.classList.toggle('active')\">\n<span class=\"toggle-text\"></span>\n</div>\n<h3>Accessible Toggle</h3>\n<accessible-toggle></accessible-toggle>\nCSS\n/* Simple Dark Mode CSS */\n/* Default light mode */\n:root {\n--bg-color: #ffffff;\n--text-color: #333333;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n}\n}\n/* Basic styling */\nbody {\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nsans-serif;\nline-height: 1.5;\nmargin: 0;\npadding: 0;\ntransition: background-color 0.3s ease, color 0.3s ease;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\nmin-height: 100vh;\ntext-align: center;\n}\n.toggle-button {\npadding: 8px 16px;\nborder: 2px solid #333;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s;\nbackground: #ccc;\ncolor: #000;\n}\n.toggle-button.active {\nbackground-color: #4caf50;\ncolor: white;\n}\n.toggle-button.active .toggle-text {\ncontent: \"ON\";\n}\n.active .toggle-text:after {\ncontent: \"ON\";\n}\n.toggle-text:after {\ncontent: \"OFF\";\n}\nJavaScript\nclass AccessibleToggle extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.pressed = false;\nthis.render();\n// A native <button> already handles Enter and Space, so one click listener is enough.\nthis.shadowRoot.querySelector('button').addEventListener('click', () => this.toggle());\n}\ntoggle() {\nthis.pressed = !this.pressed;\nthis.render();\nthis.dispatchEvent(new CustomEvent('toggle', {\nbubbles: true,\ndetail: { pressed: this.pressed } }));\n}\nrender() {\nconst button = this.shadowRoot.querySelector('button') || document.createElement('button');\nbutton.setAttribute('aria-pressed', this.pressed);\nconst label = this.getAttribute('label') || 'Toggle';\n// The name comes from the visible text; aria-pressed carries the state.\nbutton.innerHTML = `${label} <span aria-hidden=\"true\">${this.pressed ? 'ON' : 'OFF'}</span>`;\nbutton.className = this.pressed ? 'pressed' : '';\nif (!this.shadowRoot.querySelector('button')) {\nthis.shadowRoot.innerHTML = `\n<style>\nbutton {\npadding: 8px 16px;\nborder: 2px solid #333;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s;\n}\nbutton:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\nbutton.pressed {\nbackground-color: #4CAF50;\ncolor: white;\n}\nbutton:not(.pressed) {\nbackground-color: #f1f1f1;\n}\n</style>\n`;\nthis.shadowRoot.appendChild(button);\n}\n}}\ncustomElements.define('accessible-toggle', AccessibleToggle);\nAccessiblity in Web Components Example 1 on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\nTry using only your keyboard (Tab and Enter) to interact with both buttons above. Notice how the accessible version provides:\nClear focus indication\nProper state announcement\nKeyboard operability\nThe first toggle is a plain <div> with a click handler: it has no role, no keyboard stop, and nothing that says \"pressed\". For Sarah, that is a rectangle she cannot find and cannot operate. These seemingly small details make a tremendous difference for users relying on assistive technologies.\nUnderstanding Accessibility in Web Components\nNow that we've seen the tangible impact of accessibility features, let's explore the core principles that make web components truly inclusive for all users.\nAccessibility (often abbreviated as A11y) involves designing products usable by people with disabilities. For Web Components, this means ensuring custom elements are perceivable, operable, understandable, and robust for all users.\nKey Considerations:\nSemantic HTML: Use appropriate HTML elements to convey meaning, as browsers and assistive technologies rely on these semantics.\nARIA Roles and Properties: When native HTML elements are insufficient, ARIA roles and properties provide additional accessibility information.\nKeyboard Accessibility: Ensure all interactive elements are navigable and operable via keyboard.\nFocus Management: Properly manage focus to assist users navigating with keyboards or assistive devices.\nWhile these accessibility considerations apply to all web development, Web Components introduce unique challenges through their use of Shadow DOM—an essential feature that requires thoughtful implementation.\nThe Shadow DOM Challenge\nWeb Components use Shadow DOM to encapsulate styles and functionality, creating unique accessibility challenges:\nStyle Isolation: Focus styles might not be visible if they rely on global CSS\nDOM Isolation: ARIA relationships across shadow boundaries might not work as expected\nEvent Handling: Custom events must properly handle keyboard interactions\nDespite these challenges, you can create fully accessible Web Components with the right approach. Let's explore practical implementation strategies that address these concerns while maintaining the benefits of encapsulation.\nImplementing Accessibility in Web Components\nThese challenges can be overcome with proper implementation, as demonstrated in the examples below.\n1. Semantic HTML and ARIA Roles\nStart with native HTML elements that inherently provide accessibility. When creating custom elements, assign appropriate ARIA roles and properties to convey their purpose and state.\nInteractive Example: Accessible Toggle Button\nHTML\n<accessible-toggle></accessible-toggle>\nCSS\n/* Simple Dark Mode CSS */\n/* Default light mode */\n:root {\n--bg-color: #ffffff;\n--text-color: #333333;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n}\n}\n/* Basic styling */\nbody {\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nsans-serif;\nline-height: 1.5;\nmargin: 0;\npadding: 0;\ntransition: background-color 0.3s ease, color 0.3s ease;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\nmin-height: 100vh;\ntext-align: center;\n}\n.toggle-button {\npadding: 8px 16px;\nborder: 2px solid #333;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s;\nbackground: #ccc;\ncolor: #000;\n}\n.toggle-button.active {\nbackground-color: #4caf50;\ncolor: white;\n}\n.toggle-button.active .toggle-text {\ncontent: \"ON\";\n}\n.active .toggle-text:after {\ncontent: \"ON\";\n}\n.toggle-text:after {\ncontent: \"OFF\";\n}\nJavaScript\nclass AccessibleToggle extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.pressed = false;\nthis.render();\n// A native <button> already handles Enter and Space, so one click listener is enough.\nthis.shadowRoot.querySelector('button').addEventListener('click', () => this.toggle());\n}\ntoggle() {\nthis.pressed = !this.pressed;\nthis.render();\nthis.dispatchEvent(new CustomEvent('toggle', {\nbubbles: true,\ndetail: { pressed: this.pressed } }));\n}\nrender() {\nconst button = this.shadowRoot.querySelector('button') || document.createElement('button');\nbutton.setAttribute('aria-pressed', this.pressed);\nconst label = this.getAttribute('label') || 'Toggle';\n// The name comes from the visible text; aria-pressed carries the state.\nbutton.innerHTML = `${label} <span aria-hidden=\"true\">${this.pressed ? 'ON' : 'OFF'}</span>`;\nbutton.className = this.pressed ? 'pressed' : '';\nif (!this.shadowRoot.querySelector('button')) {\nthis.shadowRoot.innerHTML = `\n<style>\nbutton {\npadding: 8px 16px;\nborder: 2px solid #333;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s;\n}\nbutton:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\nbutton.pressed {\nbackground-color: #4CAF50;\ncolor: white;\n}\nbutton:not(.pressed) {\nbackground-color: #f1f1f1;\n}\n</style>\n`;\nthis.shadowRoot.appendChild(button);\n}\n}}\ncustomElements.define('accessible-toggle', AccessibleToggle);\nAccessible Toggle Switch Web Component on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\nclass AccessibleToggle extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.pressed = false;\nthis.render();\n// A native <button> already handles Enter and Space, so one click listener is enough.\nthis.shadowRoot.querySelector('button').addEventListener('click', () => this.toggle());\n}\ntoggle() {\nthis.pressed = !this.pressed;\nthis.render();\nthis.dispatchEvent(new CustomEvent('toggle', {\nbubbles: true,\ndetail: { pressed: this.pressed }\n}));\n}\nrender() {\nconst button = this.shadowRoot.querySelector('button') || document.createElement('button');\nbutton.setAttribute('aria-pressed', this.pressed);\nconst label = this.getAttribute('label') || 'Toggle';\n// The name comes from the visible text; aria-pressed carries the state.\nbutton.innerHTML = `${label} <span aria-hidden=\"true\">${this.pressed ? 'ON' : 'OFF'}</span>`;\nbutton.className = this.pressed ? 'pressed' : '';\nif (!this.shadowRoot.querySelector('button')) {\nthis.shadowRoot.innerHTML = `\n<style>\nbutton {\npadding: 8px 16px;\nborder: 2px solid #333;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s;\n}\nbutton:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\nbutton.pressed {\nbackground-color: #4CAF50;\ncolor: white;\n}\nbutton:not(.pressed) {\nbackground-color: #f1f1f1;\n}\n</style>\n`;\nthis.shadowRoot.appendChild(button);\n}\n}\n}\ncustomElements.define('accessible-toggle', AccessibleToggle);\nKey accessibility features:\nUses a real <button>, so it is reachable with Tab and works with Enter and Space with no extra code\nUses aria-pressed to convey the on/off state to assistive technologies\nLets the visible text (\"Toggle\") be the accessible name, with the ON/OFF text hidden from screen readers because aria-pressed already says it\nProvides visible state indication through color and text changes\nImplements strong focus indication with high contrast outline\nDispatches events that can be caught by parent components\nTry navigating to the button with Tab and activating it with Enter or Space. A screen reader typically announces something like \"Toggle, toggle button, not pressed\" and, after activation, \"Toggle, toggle button, pressed\". The exact wording varies between screen readers.\nA mistake worth seeing. An earlier version of this toggle set role=\"button\" and aria-label=\"Toggle\" on the <button>. Both were harmful in small ways. role=\"button\" on a <button> repeats what the browser already knows. And aria-label replaces the visible text as the button's name, so the screen reader never says \"ON\" or \"OFF\", and a voice-control user who says \"click ON\" may find nothing with that name. In Chrome's accessibility tree the old version showed the name Toggle with the visible text ON sitting under it; the new one has the name Toggle and the state pressed, and the visible text agrees with what is announced. The rule behind it: do not override a visible label with a different aria-label.\nQuestion: A custom toggle is built from a native <button>. Which attribute should carry its on/off state for assistive technology?\nrole=\"button\"\ntabindex=\"0\"\naria-pressed\nAnswer: 3. aria-pressed\nExplanation: aria-pressed=\"true\" or \"false\" tells assistive technology the button is a toggle and which way it is set. role=\"button\" is already implied by the element, and tabindex=\"0\" only affects Tab order, which a native button already has.\n2. Keyboard Accessibility and Focus Management\nWith the foundation of proper semantics established, let's focus on how users navigate through interactive components without a mouse.\nProper focus management matters, especially with dynamic content or modal dialogs. Users navigating with keyboards need clear focus indication and logical tab order.\nTwo terms before the next example. Focus is the element that currently receives keystrokes; Tab moves it to the next interactive element. A focus trap is what a modal dialog does while it is open: Tab and Shift+Tab cycle among the controls inside the dialog instead of wandering to the page hidden behind it. When the dialog closes, focus goes back to the button that opened it.\nA note from October 2026: a native <dialog> opened with showModal() already traps focus, closes on Escape and returns focus to the opener, so for a plain modal you may not need a class like the one below. We build it by hand here to see what the browser does on our behalf.\nInteractive Example: Accessible Modal Dialog\nHTML\n<accessible-modal></accessible-modal>\nCSS\n/* Simple Dark Mode CSS */\n/* Default light mode */\n:root {\n--bg-color: #ffffff;\n--text-color: #333333;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n}\n}\n/* Basic styling */\nbody {\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nsans-serif;\nline-height: 1.5;\nmargin: 0;\npadding: 0;\ntransition: background-color 0.3s ease, color 0.3s ease;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\nmin-height: 100vh;\ntext-align: center;\n}\nJavaScript\nclass AccessibleModal extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis._previouslyFocusedElement = null;\nthis._keydownHandler = null;\nthis._escapeHandler = null;\nthis.render();\n}\nconnectedCallback() {\nconst trigger = this.shadowRoot.querySelector(\"#modal-trigger\");\nconst closeBtn = this.shadowRoot.querySelector(\"#modal-close\");\nconst dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\nconst overlay = this.shadowRoot.querySelector(\".modal-overlay\");\n// Open modal on trigger click\ntrigger.addEventListener(\"click\", () => this.open());\n// Close modal on close button click\ncloseBtn.addEventListener(\"click\", () => this.close());\n// Create handler for Escape key\nthis._escapeHandler = e => {\nif (e.key === \"Escape\" && dialog.getAttribute(\"aria-hidden\") !== \"true\") {\nthis.close();\n}\n};\n// Add Escape key event listener to the document\ndocument.addEventListener(\"keydown\", this._escapeHandler);\n// Close when clicking outside the modal\noverlay.addEventListener(\"click\", e => {\nif (e.target === overlay) {\nthis.close();\n}\n});\n}\ndisconnectedCallback() {\n// Clean up event listeners when component is removed\nif (this._escapeHandler) {\ndocument.removeEventListener(\"keydown\", this._escapeHandler);\n}\nif (this._keydownHandler) {\ndocument.removeEventListener(\"keydown\", this._keydownHandler);\n}\n}\nopen() {\n// Store currently focused element to restore later\nthis._previouslyFocusedElement = document.activeElement;\nconst dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\nconst overlay = this.shadowRoot.querySelector(\".modal-overlay\");\n// Make modal visible\ndialog.removeAttribute(\"aria-hidden\");\noverlay.style.display = \"flex\";\n// Focus the first focusable element (close button)\nsetTimeout(() => {\nthis.shadowRoot.querySelector(\"#modal-close\").focus();\n}, 50);\n// Set up focus trapping\nthis._setupFocusTrap();\n// Prevent background scrolling\ndocument.body.style.overflow = \"hidden\";\n}\nclose() {\nconst dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\nconst overlay = this.shadowRoot.querySelector(\".modal-overlay\");\n// Hide modal\ndialog.setAttribute(\"aria-hidden\", \"true\");\noverlay.style.display = \"none\";\n// Remove focus trap\nif (this._keydownHandler) {\ndocument.removeEventListener(\"keydown\", this._keydownHandler);\nthis._keydownHandler = null;\n}\n// Re-enable scrolling\ndocument.body.style.overflow = \"\";\n// Restore focus with a slight delay to ensure UI updates first\nif (this._previouslyFocusedElement) {\nconsole.log(\"Restoring focus to:\", this._previouslyFocusedElement);\nsetTimeout(() => {\ntry {\nthis.shadowRoot.querySelector(\"#modal-trigger\").focus();\n} catch (e) {\nconsole.error(\"Error restoring focus:\", e);\n// Fallback - focus the trigger button\nthis.shadowRoot.querySelector(\"#modal-trigger\").focus();\n}\n}, 100);\n} else {\n// Fallback if no previous element was stored\nsetTimeout(() => {\nthis.shadowRoot.querySelector(\"#modal-trigger\").focus();\n}, 100);\n}\n}\n_setupFocusTrap() {\n// Get all focusable elements within the modal\nconst focusableElements = Array.from(\nthis.shadowRoot.querySelectorAll(\n'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])'));\nconst firstFocusable = focusableElements[0];\nconst lastFocusable = focusableElements[focusableElements.length - 1];\n// Remove any previous event listener\nif (this._keydownHandler) {\ndocument.removeEventListener(\"keydown\", this._keydownHandler);\n}\n// Create and add the keydown handler for Tab key focus trapping\nthis._keydownHandler = e => {\n// Only respond to Tab key\nif (e.key === \"Tab\") {\n// If pressing Shift+Tab on the first focusable element, wrap to the last\nif (e.shiftKey && document.activeElement === firstFocusable) {\ne.preventDefault();\nlastFocusable.focus();\n}\n// If pressing Tab on the last focusable element, wrap to the first\nelse if (!e.shiftKey && document.activeElement === lastFocusable) {\ne.preventDefault();\nfirstFocusable.focus();\n}\n}\n};\n// Add the event listener to the document (more reliable than shadowRoot)\ndocument.addEventListener(\"keydown\", this._keydownHandler);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n/* Dark mode variables */\n:host {\n--bg-color: #ffffff;\n--text-color: #333333;\n--overlay-bg: rgba(0, 0, 0, 0.5);\n--modal-bg: #ffffff;\n--modal-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n--button-border: #ccc;\n--button-bg: #f5f5f5;\n--button-hover: #e0e0e0;\n--button-focus: blue;\n--primary-button-bg: #4CAF50;\n--primary-button-hover: #3d8b40;\n--primary-button-color: white;\n/* Center content */\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\ntext-align: center;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:host {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--overlay-bg: rgba(0, 0, 0, 0.7);\n--modal-bg: #1e1e1e;\n--modal-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n--button-border: #444;\n--button-bg: #2d2d2d;\n--button-hover: #3d3d3d;\n--button-focus: #7289da;\n--primary-button-bg: #2e7d32;\n--primary-button-hover: #266d29;\n--primary-button-color: #e0e0e0;\n}\n}\n.modal-overlay {\nposition: fixed;\ntop: 0;\nleft: 0;\nright: 0;\nbottom: 0;\nbackground-color: var(--overlay-bg);\ndisplay: none;\njustify-content: center;\nalign-items: center;\nz-index: 1000;\n}\n.modal-content {\nbackground-color: var(--modal-bg);\ncolor: var(--text-color);\npadding: 20px;\nborder-radius: 4px;\nmax-width: 500px;\nwidth: 90%;\nbox-shadow: var(--modal-shadow);\ntransition: background-color 0.3s ease, color 0.3s ease;\n}\nbutton {\npadding: 8px 16px;\ncursor: pointer;\nborder-radius: 4px;\nbackground-color: var(--button-bg);\ncolor: var(--text-color);\nborder: 1px solid var(--button-border);\ntransition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;\n}\nbutton:hover {\nbackground-color: var(--button-hover);\n}\nbutton:focus-visible {\noutline: 3px solid var(--button-focus);\noutline-offset: 2px;\n}\n#modal-close {\nfloat: right;\nbackground: none;\nborder: 1px solid var(--button-border);\nfont-size: 16px;\n}\n#modal-trigger {\nbackground-color: var(--primary-button-bg);\ncolor: var(--primary-button-color);\nborder: none;\nfont-size: 16px;\nmargin: 20px 0;\n}\n#modal-trigger:hover {\nopacity: 0.9;\nbackground-color: var(--primary-button-hover);\n}\n.button-container {\ndisplay: flex;\njustify-content: center;\ngap: 10px;\nmargin-top: 15px;\n}\n</style>\n<button id=\"modal-trigger\">Open Modal</button>\n<div class=\"modal-overlay\">\n<div role=\"dialog\" aria-labelledby=\"modal-title\" aria-modal=\"true\" aria-hidden=\"true\" class=\"modal-content\">\n<button id=\"modal-close\" aria-label=\"Close dialog\">×</button>\n<h2 id=\"modal-title\">Accessible Modal Dialog</h2>\n<p>This is an example of an accessible modal dialog component.</p>\n<p>It traps focus inside the modal, can be closed with the ESC key, and restores focus when closed.</p>\n<div class=\"button-container\">\n<button>Test Button 1</button>\n<button>Test Button 2</button>\n</div>\n</div>\n</div>\n`;\n}}\ncustomElements.define(\"accessible-modal\", AccessibleModal);\nAccessible Modal Web Component on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\n<details>\n<summary>Accessible modal JavaScript · 138 lines</summary>\nclass AccessibleModal extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis._previouslyFocusedElement = null;\nthis.render();\n}\nconnectedCallback() {\nconst trigger = this.shadowRoot.querySelector('#modal-trigger');\nconst closeBtn = this.shadowRoot.querySelector('#modal-close');\nconst dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\ntrigger.addEventListener('click', () => this.open());\ncloseBtn.addEventListener('click', () => this.close());\n// Close on Escape key\nthis.addEventListener('keydown', e => {\nif (e.key === 'Escape' && !dialog.hasAttribute('aria-hidden')) {\nthis.close();\n}\n});\n// Close when clicking outside the modal\nthis.shadowRoot.querySelector('.modal-overlay').addEventListener('click', e => {\nif (e.target.classList.contains('modal-overlay')) {\nthis.close();\n}\n});\n}\nopen() {\nthis._previouslyFocusedElement = document.activeElement;\nconst dialog = this.shadowRoot.querySelector('[role=\"dialog\"]');\ndialog.removeAttribute('aria-hidden');\nthis.shadowRoot.querySelector('.modal-overlay').style.display = 'flex';\nthis.shadowRoot.querySelector('#modal-close').focus();\n// Trap focus inside modal\nthis._handleFocusTrap();\n}\nclose() {\nthis.shadowRoot.querySelector('[role=\"dialog\"]').setAttribute('aria-hidden', 'true');\nthis.shadowRoot.querySelector('.modal-overlay').style.display = 'none';\n// Restore focus\nif (this._previouslyFocusedElement) {\nthis._previouslyFocusedElement.focus();\n}\n}\n_handleFocusTrap() {\nconst focusableElements = this.shadowRoot.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])');\nconst firstFocusable = focusableElements[0];\nconst lastFocusable = focusableElements[focusableElements.length - 1];\nthis.shadowRoot.addEventListener('keydown', e => {\nif (e.key === 'Tab') {\nif (e.shiftKey && document.activeElement === firstFocusable) {\ne.preventDefault();\nlastFocusable.focus();\n} else if (!e.shiftKey && document.activeElement === lastFocusable) {\ne.preventDefault();\nfirstFocusable.focus();\n}\n}\n});\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.modal-overlay {\nposition: fixed;\ntop: 0;\nleft: 0;\nright: 0;\nbottom: 0;\nbackground-color: rgba(0, 0, 0, 0.5);\ndisplay: none;\njustify-content: center;\nalign-items: center;\nz-index: 1000;\n}\n.modal-content {\nbackground-color: white;\npadding: 20px;\nborder-radius: 4px;\nmax-width: 500px;\nwidth: 90%;\nbox-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\nbutton {\npadding: 8px 16px;\ncursor: pointer;\nborder-radius: 4px;\n}\nbutton:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\n#modal-close {\nfloat: right;\nbackground: none;\nborder: 1px solid #ccc;\nfont-size: 16px;\n}\n#modal-trigger {\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nfont-size: 16px;\n}\n</style>\n<button id=\"modal-trigger\">Open Modal</button>\n<div class=\"modal-overlay\">\n<div role=\"dialog\" aria-labelledby=\"modal-title\" aria-modal=\"true\" aria-hidden=\"true\" class=\"modal-content\">\n<button id=\"modal-close\" aria-label=\"Close dialog\">×</button>\n<h2 id=\"modal-title\">Accessible Modal Dialog</h2>\n<p>This is an example of an accessible modal dialog component.</p>\n<p>It traps focus inside the modal, can be closed with the ESC key, and restores focus when closed.</p>\n<button>Test Button 1</button>\n<button>Test Button 2</button>\n</div>\n</div>\n`;\n}\n}\ncustomElements.define('accessible-modal', AccessibleModal);\n</details>\nKey accessibility features:\nFocus trap within the modal (users can't tab out accidentally)\nKeyboard closing with Escape key\nFocus restoration when modal closes\nProper ARIA roles (dialog, aria-labelledby, aria-modal)\nClear visual focus indicators\nTry opening the modal and navigating with Tab. Notice how focus is trapped inside the modal and restored to the trigger button when closed.\n3. Complex Components and Keyboard Navigation\nWhile proper keyboard navigation creates a solid foundation, complex components like tabs, accordions, and carousels require specialized interaction patterns to maintain usability.\nThe tabs below use tabindex to keep the whole tab list to a single stop in the Tab order. tabindex=\"0\" puts an element in the normal Tab order. tabindex=\"-1\" takes it out of the Tab order but still lets a script focus it with .focus(), which is what the arrow keys do here. Only the selected tab gets 0; the others get -1. Tab enters the tab list once, and the arrow keys move around inside it.\nInteractive Example: Accessible Tabs\nHTML\n<accessible-tabs></accessible-tabs>\nCSS\n/* Simple Dark Mode CSS */\n/* Default light mode */\n:root {\n--bg-color: #ffffff;\n--text-color: #333333;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n}\n}\n/* Basic styling */\nbody {\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nsans-serif;\nline-height: 1.5;\nmargin: 0;\npadding: 0;\ntransition: background-color 0.3s ease, color 0.3s ease;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\nmin-height: 100vh;\ntext-align: center;\n}\n.toggle-button {\npadding: 8px 16px;\nborder: 2px solid #333;\nborder-radius: 4px;\nfont-size: 16px;\ncursor: pointer;\ntransition: all 0.3s;\nbackground: #ccc;\ncolor: #000;\n}\n.toggle-button.active {\nbackground-color: #4caf50;\ncolor: white;\n}\n.toggle-button.active .toggle-text {\ncontent: \"ON\";\n}\n.active .toggle-text:after {\ncontent: \"ON\";\n}\n.toggle-text:after {\ncontent: \"OFF\";\n}\nJavaScript\nclass AccessibleTabs extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.render();\n}\nconnectedCallback() {\nconst tabs = Array.from(this.shadowRoot.querySelectorAll('[role=\"tab\"]'));\nconst panels = Array.from(this.shadowRoot.querySelectorAll('[role=\"tabpanel\"]'));\n// Select first tab by default\nthis.selectTab(tabs[0]);\ntabs.forEach(tab => {\ntab.addEventListener('click', () => this.selectTab(tab));\ntab.addEventListener('keydown', e => {\nif (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {\ne.preventDefault();\nconst currentIndex = tabs.indexOf(tab);\nlet nextIndex;\nif (e.key === 'ArrowRight') {\nnextIndex = (currentIndex + 1) % tabs.length;\n} else {\nnextIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n}\ntabs[nextIndex].focus();\nthis.selectTab(tabs[nextIndex]);\n}\n});\n});\n}\nselectTab(selectedTab) {\nconst tabs = Array.from(this.shadowRoot.querySelectorAll('[role=\"tab\"]'));\nconst panels = Array.from(this.shadowRoot.querySelectorAll('[role=\"tabpanel\"]'));\ntabs.forEach(tab => {\nconst selected = tab === selectedTab;\ntab.setAttribute('aria-selected', selected);\ntab.setAttribute('tabindex', selected ? '0' : '-1');\n});\npanels.forEach(panel => {\nconst isSelected = panel.id === selectedTab.getAttribute('aria-controls');\npanel.hidden = !isSelected;\n});\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.tabs {\ndisplay: flex;\nborder-bottom: 1px solid #ccc;\n}\n[role=\"tab\"] {\npadding: 10px 15px;\nmargin-right: 5px;\nbackground-color: #f1f1f1;\nborder: 1px solid #ccc;\nborder-bottom: none;\nborder-radius: 4px 4px 0 0;\ncursor: pointer;\nposition: relative;\ntop: 1px;\n}\n[role=\"tab\"][aria-selected=\"true\"] {\nbackground-color: white;\nborder-bottom: 1px solid white;\n}\n[role=\"tab\"]:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\n[role=\"tabpanel\"] {\npadding: 20px;\nborder: 1px solid #ccc;\nborder-top: none;\n}\n</style>\n<div class=\"tabs-container\">\n<div role=\"tablist\" class=\"tabs\" aria-label=\"Programming information\">\n<button role=\"tab\" id=\"tab-1\" aria-selected=\"true\" aria-controls=\"panel-1\" tabindex=\"0\">HTML</button>\n<button role=\"tab\" id=\"tab-2\" aria-selected=\"false\" aria-controls=\"panel-2\" tabindex=\"-1\">CSS</button>\n<button role=\"tab\" id=\"tab-3\" aria-selected=\"false\" aria-controls=\"panel-3\" tabindex=\"-1\">JavaScript</button>\n</div>\n<div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\">\n<h3>HTML</h3>\n<p>HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.</p>\n</div>\n<div role=\"tabpanel\" id=\"panel-2\" aria-labelledby=\"tab-2\" hidden>\n<h3>CSS</h3>\n<p>CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in HTML.</p>\n</div>\n<div role=\"tabpanel\" id=\"panel-3\" aria-labelledby=\"tab-3\" hidden>\n<h3>JavaScript</h3>\n<p>JavaScript is a programming language that enables interactive web pages and is an essential part of web applications.</p>\n</div>\n</div>\n`;\n}}\ncustomElements.define('accessible-tabs', AccessibleTabs);\nAccessible Tabs Web Component on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\n<details>\n<summary>Accessible tabs JavaScript · 131 lines</summary>\nclass AccessibleTabs extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.render();\n}\nconnectedCallback() {\nconst tabs = Array.from(this.shadowRoot.querySelectorAll('[role=\"tab\"]'));\nconst panels = Array.from(this.shadowRoot.querySelectorAll('[role=\"tabpanel\"]'));\n// Select first tab by default\nthis.selectTab(tabs[0]);\ntabs.forEach(tab => {\ntab.addEventListener('click', () => this.selectTab(tab));\ntab.addEventListener('keydown', e => {\n// Arrow key navigation\nif (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {\ne.preventDefault();\nconst currentIndex = tabs.indexOf(tab);\nlet nextIndex;\nif (e.key === 'ArrowRight') {\nnextIndex = (currentIndex + 1) % tabs.length;\n} else {\nnextIndex = (currentIndex - 1 + tabs.length) % tabs.length;\n}\ntabs[nextIndex].focus();\nthis.selectTab(tabs[nextIndex]);\n}\n// Home and End keys for first/last tabs\nif (e.key === 'Home') {\ne.preventDefault();\ntabs[0].focus();\nthis.selectTab(tabs[0]);\n}\nif (e.key === 'End') {\ne.preventDefault();\ntabs[tabs.length - 1].focus();\nthis.selectTab(tabs[tabs.length - 1]);\n}\n});\n});\n}\nselectTab(selectedTab) {\nconst tabs = Array.from(this.shadowRoot.querySelectorAll('[role=\"tab\"]'));\nconst panels = Array.from(this.shadowRoot.querySelectorAll('[role=\"tabpanel\"]'));\ntabs.forEach(tab => {\nconst selected = tab === selectedTab;\ntab.setAttribute('aria-selected', selected);\ntab.setAttribute('tabindex', selected ? '0' : '-1');\n});\npanels.forEach(panel => {\nconst isSelected = panel.id === selectedTab.getAttribute('aria-controls');\npanel.hidden = !isSelected;\n});\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.tabs {\ndisplay: flex;\nborder-bottom: 1px solid #ccc;\n}\n[role=\"tab\"] {\npadding: 10px 15px;\nmargin-right: 5px;\nbackground-color: #f1f1f1;\nborder: 1px solid #ccc;\nborder-bottom: none;\nborder-radius: 4px 4px 0 0;\ncursor: pointer;\nposition: relative;\ntop: 1px;\n}\n[role=\"tab\"][aria-selected=\"true\"] {\nbackground-color: white;\nborder-bottom: 1px solid white;\n}\n[role=\"tab\"]:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\n[role=\"tabpanel\"] {\npadding: 20px;\nborder: 1px solid #ccc;\nborder-top: none;\n}\n</style>\n<div class=\"tabs-container\">\n<div role=\"tablist\" class=\"tabs\" aria-label=\"Programming information\">\n<button role=\"tab\" id=\"tab-1\" aria-selected=\"true\" aria-controls=\"panel-1\" tabindex=\"0\">HTML</button>\n<button role=\"tab\" id=\"tab-2\" aria-selected=\"false\" aria-controls=\"panel-2\" tabindex=\"-1\">CSS</button>\n<button role=\"tab\" id=\"tab-3\" aria-selected=\"false\" aria-controls=\"panel-3\" tabindex=\"-1\">JavaScript</button>\n</div>\n<div role=\"tabpanel\" id=\"panel-1\" aria-labelledby=\"tab-1\">\n<h3>HTML</h3>\n<p>HTML (HyperText Markup Language) is the standard markup language for documents designed to be displayed in a web browser.</p>\n</div>\n<div role=\"tabpanel\" id=\"panel-2\" aria-labelledby=\"tab-2\" hidden>\n<h3>CSS</h3>\n<p>CSS (Cascading Style Sheets) is a style sheet language used for describing the presentation of a document written in HTML.</p>\n</div>\n<div role=\"tabpanel\" id=\"panel-3\" aria-labelledby=\"tab-3\" hidden>\n<h3>JavaScript</h3>\n<p>JavaScript is a programming language that enables interactive web pages and is an essential part of web applications.</p>\n</div>\n</div>\n`;\n}\n}\ncustomElements.define('accessible-tabs', AccessibleTabs);\n</details>\nKey accessibility features:\nProper ARIA roles for tablist, tab, and tabpanel\nKeyboard navigation with arrow keys, Home, and End\nSingle tab stop in the tab order with arrow key navigation between tabs\nProper state management with aria-selected and tabindex\nClear relationship between tabs and panels using aria-controls and aria-labelledby\nTry using the arrow keys to navigate between tabs. Notice how this follows the expected keyboard interaction pattern for tabs.\n4. Announcements for Dynamic Content\nBeyond static structure and interaction, many modern interfaces feature dynamic content that changes without page reloads. This presents another accessibility challenge: how do we keep non-visual users informed of these changes?\nThink of Sarah pressing a button that adds an item to a cart. Her focus stays on the button, and a \"1 item in cart\" message appears elsewhere on the screen. Nothing told her. An element marked aria-live is a live region: when its text changes, the screen reader speaks the new text even though focus is somewhere else. aria-live=\"polite\" waits for a pause in speech; aria-live=\"assertive\" interrupts, so save it for urgent messages. The region should already be on the page before you change its text.\nHTML\n<live-region></live-region>\nCSS\n/* Simple Dark Mode CSS */\n/* Default light mode */\n:root {\n--bg-color: #ffffff;\n--text-color: #333333;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n}\n}\n/* Basic styling */\nbody {\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nsans-serif;\nline-height: 1.5;\nmargin: 0;\npadding: 0;\ntransition: background-color 0.3s ease, color 0.3s ease;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\nmin-height: 100vh;\ntext-align: center;\n}\nJavaScript\nclass LiveRegion extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.render();\n}\nconnectedCallback() {\nconst button = this.shadowRoot.querySelector('button');\nconst liveRegion = this.shadowRoot.querySelector('[aria-live]');\nlet count = 0;\n// Listen for theme changes\nthis.updateTheme = () => {\nconst isDarkMode = this.shadowRoot.host.hasAttribute('dark-mode');\nthis.shadowRoot.querySelector('.container').classList.toggle('dark-theme', isDarkMode);\n};\n// Check for system preference initially\nconst prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\nif (prefersDarkMode) {\nthis.shadowRoot.host.setAttribute('dark-mode', '');\nthis.updateTheme();\n}\n// Listen for system preference changes\nwindow.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {\nif (e.matches) {\nthis.shadowRoot.host.setAttribute('dark-mode', '');\n} else {\nthis.shadowRoot.host.removeAttribute('dark-mode');\n}\nthis.updateTheme();\n});\n// Button click event\nbutton.addEventListener('click', () => {\ncount++;\nliveRegion.textContent = `Button clicked ${count} time${count === 1 ? '' : 's'}`;\n});\n// Theme toggle event\n//     themeToggle.addEventListener('click', () => {\n//       if (this.shadowRoot.host.hasAttribute('dark-mode')) {\n//         this.shadowRoot.host.removeAttribute('dark-mode');\n//       } else {\n//         this.shadowRoot.host.setAttribute('dark-mode', '');\n//       }\n//       this.updateTheme();\n//       // Announce theme change to screen readers\n//       const mode = this.shadowRoot.host.hasAttribute('dark-mode') ? 'dark' : 'light';\n//       const announcement = this.shadowRoot.querySelector('.theme-announcement');\n//       announcement.textContent = `Theme changed to ${mode} mode`;\n//     });\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.container {\npadding: 20px;\nborder-radius: 8px;\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nbackground-color: #ffffff;\ncolor: #333333;\ntransition: background-color 0.3s, color 0.3s;\n}\n.container.dark-theme {\nbackground-color: #1a1a1a;\ncolor: #e0e0e0;\n}\nbutton {\npadding: 8px 16px;\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, transform 0.1s;\n}\n.dark-theme button {\nbackground-color: #3a8a3d;\n}\nbutton:hover {\nopacity: 0.9;\n}\nbutton:active {\ntransform: scale(0.98);\n}\nbutton:focus-visible {\noutline: 3px solid #7cc0ff;\noutline-offset: 2px;\n}\n.dark-theme button:focus-visible {\noutline-color: #4a8dcb;\n}\n#theme-toggle {\nbackground-color: #6c757d;\nmargin-left: 10px;\n}\n.dark-theme #theme-toggle {\nbackground-color: #495057;\n}\n.live-region {\nmargin-top: 15px;\npadding: 12px;\nbackground-color: #f8f8f8;\nborder-radius: 6px;\nborder-left: 4px solid #4CAF50;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.05);\nfont-size: 16px;\n}\n.dark-theme .live-region {\nbackground-color: #2a2a2a;\nborder-left-color: #3a8a3d;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.2);\n}\n.hidden {\nposition: absolute;\nwidth: 1px;\nheight: 1px;\npadding: 0;\nmargin: -1px;\noverflow: hidden;\nclip: rect(0, 0, 0, 0);\nwhite-space: nowrap;\nborder-width: 0;\n}\n</style>\n<div class=\"container\">\n<div class=\"controls\">\n<button>Increment Counter</button>\n</div>\n<div class=\"live-region\" aria-live=\"polite\">Counter starts at 0</div>\n<!-- Hidden live region for theme announcements -->\n<div class=\"theme-announcement hidden\" aria-live=\"polite\"></div>\n</div>\n`;\n}}\ncustomElements.define('live-region', LiveRegion);\nLive Region Web Component Example on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\nclass LiveRegion extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.render();\n}\nconnectedCallback() {\nconst button = this.shadowRoot.querySelector('button');\nconst liveRegion = this.shadowRoot.querySelector('[aria-live]');\nlet count = 0;\nbutton.addEventListener('click', () => {\ncount++;\nliveRegion.textContent = `Button clicked ${count} time${count === 1 ? '' : 's'}`;\n});\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\nbutton {\npadding: 8px 16px;\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\nbutton:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\n.live-region {\nmargin-top: 10px;\npadding: 10px;\nbackground-color: #f8f8f8;\nborder-radius: 4px;\n}\n</style>\n<div>\n<button>Increment Counter</button>\n<div class=\"live-region\" aria-live=\"polite\">Counter starts at 0</div>\n</div>\n`;\n}\n}\ncustomElements.define('live-region', LiveRegion);\nKey accessibility feature:\nUses aria-live=\"polite\" to announce changes to screen readers without interrupting current announcements\nQuestion: Focus stays on a button while the page changes. Which of these updates needs an aria-live region for a screen reader user to hear about it?\nPressing Tab moves focus to the next link\nA dialog opens and the script moves focus onto its heading\nA \"Saved\" message appears next to the button while focus stays on the button\nAnswer: 3. A \"Saved\" message appears next to the button while focus stays on the button\nExplanation: Moving focus already makes the screen reader speak the newly focused element, so the first two announce themselves. A message that appears elsewhere while focus stays put is silent unless it sits in a live region.\nTesting for Accessibility\nImplementing these accessibility features is only half the journey. To ensure your components work reliably for all users, you need a systematic approach to testing. Let's explore the tools and techniques that will help you verify your implementation meets accessibility standards.\n1. Automated Testing\nUtilize tools like axe-core (https://www.deque.com/axe/) or Lighthouse (https://developers.google.com/web/tools/lighthouse):\n// Example of using axe-core for accessibility testing\nimport axe from 'axe-core';\n// Test a specific web component\naxe.run(document.querySelector('accessible-tabs'), (err, results) => {\nif (err) {\nconsole.error('Accessibility testing failed:', err);\n} else {\nconsole.log('Accessibility test results:', results);\n}\n});\nLighthouse is an open-source automated tool developed by Google that audits web pages for performance, accessibility, SEO, and more. There are three ways to run it for accessibility:\nMethod 1: Using Chrome DevTools\nOpen Chrome browser\nNavigate to the webpage you want to test\nPress F12 or right-click and select \"Inspect\" to open DevTools\nClick on the \"Lighthouse\" tab\nSelect \"Accessibility\" checkbox (deselect others if you only want accessibility)\nClick \"Generate report\"\nMethod 2: Chrome Extension\nInstall the Lighthouse extension from Chrome Web Store\nNavigate to the page you want to test\nClick the Lighthouse icon in your browser toolbar\nSelect \"Accessibility\" checkbox\nClick \"Generate report\"\nMethod 3: Command Line\n# Install Lighthouse globally\nnpm install -g lighthouse\n# Run Lighthouse accessibility audit\nlighthouse https://example.com --only-categories=accessibility --output=html --output-path=./report.html\n2. Manual Testing\nAutomated tests can't catch everything. Manual testing should include:\nKeyboard Testing:\nCan all interactive elements be accessed via Tab key?\nDo all controls work with keyboard alternatives (Enter, Space, arrow keys)?\nIs there a visible focus indicator at all times?\nScreen Reader Testing:\nTry your components with VoiceOver (macOS), NVDA or JAWS (Windows), or TalkBack (Android)\nIs all relevant information announced?\nAre state changes communicated properly?\nHigh Contrast Mode:\nTest with high contrast mode enabled in your OS\nDo all interactive elements remain visible and distinct?\n3. Optional: A Screen Reader Simulator\nThis section is optional. The simulator below is long, so you can skip to \"Testing with NVDA\" and lose nothing the rest of the post depends on. This interactive example demonstrates how screen readers interpret web content, without requiring any software installation. Simply enter your HTML or use the built-in generator to compare non-semantic versus semantic markup. See firsthand how thoughtful HTML structure dramatically changes the experience for users who rely on assistive technology. Test your own code to identify potential accessibility improvements. Note: This simulator provides educational insights only and should not replace testing with actual screen reader technology when evaluating web accessibility.\nHTML\n<h1>Accessibility Screen Reader Simulator</h1>\n<p>Test how a screen reader would interpret your HTML content</p>\n<!-- Use the web component anywhere in your HTML -->\n<a11y-tester></a11y-tester>\nJavaScript\n// A11y Tester Web Component\n// This file defines a custom HTML element <a11y-tester> that can be used anywhere in your HTML\nclass AccessibilityTester extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n// Create styles\nconst style = document.createElement('style');\nstyle.textContent = `\n:host {\ndisplay: block;\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nborder: 1px solid #ddd;\nborder-radius: 5px;\noverflow: hidden;\n}\n.container {\ndisplay: flex;\nflex-direction: column;\nmin-height: 400px;\n}\n.input-section, .output-section {\npadding: 15px;\n}\n.input-section {\nbackground-color: white;\nborder-bottom: 1px solid #ddd;\n}\n.output-section {\nbackground-color: #f9f9f9;\n}\nh2 {\nmargin-top: 0;\nmargin-bottom: 15px;\nfont-size: 18px;\ncolor: #2c3e50;\n}\ntextarea {\nwidth: 100%;\nmin-height: 150px;\npadding: 10px;\nborder: 1px solid #ddd;\nborder-radius: 4px;\nfont-family: monospace;\nfont-size: 14px;\nresize: vertical;\n}\nbutton {\nbackground-color: #3498db;\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\nmargin-top: 10px;\ncursor: pointer;\nfont-weight: 500;\nmargin-right: 8px;\n}\nbutton:hover {\nbackground-color: #2980b9;\n}\nbutton:disabled {\nbackground-color: #95a5a6;\ncursor: not-allowed;\n}\n.controls {\ndisplay: flex;\n}\n.a11y-result {\nborder: 1px solid #ddd;\nborder-radius: 4px;\nmargin-bottom: 15px;\nbackground-color: white;\n}\n.a11y-result-header {\npadding: 8px 12px;\nbackground-color: #e9e9e9;\nfont-weight: 500;\nborder-bottom: 1px solid #ddd;\n}\n.a11y-result-content {\npadding: 12px;\n}\n.a11y-element {\npadding: 10px;\nmargin-bottom: 8px;\nborder-left: 3px solid #3498db;\nbackground-color: rgba(52, 152, 219, 0.1);\n}\n.a11y-element.speaking {\nbackground-color: rgba(46, 204, 113, 0.2);\nborder-left-color: #2ecc71;\n}\n.a11y-element-type {\nfont-weight: 500;\nmargin-bottom: 5px;\n}\n.a11y-element-announcement {\nfont-family: monospace;\ncolor: #333;\n}\n.a11y-warnings {\nmargin-top: 15px;\n}\n.a11y-warning {\npadding: 8px 12px;\nmargin-bottom: 8px;\nbackground-color: rgba(243, 156, 18, 0.1);\nborder-left: 3px solid #f39c12;\n}\n.sample-buttons {\ndisplay: flex;\nmargin-top: 10px;\nmargin-bottom: 10px;\n}\n.sample-button {\nbackground-color: #2ecc71;\nmargin-right: 8px;\n}\n.sample-button:hover {\nbackground-color: #27ae60;\n}\n.info-text {\nfont-size: 13px;\ncolor: #7f8c8d;\nmargin-top: 5px;\nfont-style: italic;\n}\n`;\n// Create HTML structure\nconst container = document.createElement('div');\ncontainer.className = 'container';\n// Input section\nconst inputSection = document.createElement('div');\ninputSection.className = 'input-section';\nconst inputTitle = document.createElement('h2');\ninputTitle.textContent = 'HTML Input';\n// Sample buttons\nconst sampleButtons = document.createElement('div');\nsampleButtons.className = 'sample-buttons';\nthis.nonSemanticButton = document.createElement('button');\nthis.nonSemanticButton.className = 'sample-button non-semantic-button';\nthis.nonSemanticButton.textContent = 'Insert Non-Semantic Example';\nthis.semanticButton = document.createElement('button');\nthis.semanticButton.className = 'sample-button semantic-button';\nthis.semanticButton.textContent = 'Insert Semantic Example';\nsampleButtons.appendChild(this.nonSemanticButton);\nsampleButtons.appendChild(this.semanticButton);\nconst infoText = document.createElement('div');\ninfoText.className = 'info-text';\ninfoText.textContent = 'Compare how screen readers interpret semantic vs. non-semantic HTML';\nthis.htmlInput = document.createElement('textarea');\nthis.htmlInput.placeholder = 'Paste your HTML here or use sample buttons above...';\nconst controls = document.createElement('div');\ncontrols.className = 'controls';\nthis.testButton = document.createElement('button');\nthis.testButton.className = 'test-button';\nthis.testButton.textContent = 'Test Accessibility';\nthis.playButton = document.createElement('button');\nthis.playButton.className = 'play-button';\nthis.playButton.textContent = 'Simulate Screen Reader';\nthis.playButton.disabled = true;\nthis.stopButton = document.createElement('button');\nthis.stopButton.className = 'stop-button';\nthis.stopButton.textContent = 'Stop';\nthis.stopButton.disabled = true;\ncontrols.appendChild(this.testButton);\ncontrols.appendChild(this.playButton);\ncontrols.appendChild(this.stopButton);\ninputSection.appendChild(inputTitle);\ninputSection.appendChild(sampleButtons);\ninputSection.appendChild(infoText);\ninputSection.appendChild(this.htmlInput);\ninputSection.appendChild(controls);\n// Output section\nconst outputSection = document.createElement('div');\noutputSection.className = 'output-section';\nconst outputTitle = document.createElement('h2');\noutputTitle.textContent = 'Screen Reader Simulation';\nthis.outputContainer = document.createElement('div');\nthis.outputContainer.className = 'output-container';\nthis.warningsContainer = document.createElement('div');\nthis.warningsContainer.className = 'a11y-warnings';\noutputSection.appendChild(outputTitle);\noutputSection.appendChild(this.outputContainer);\noutputSection.appendChild(this.warningsContainer);\n// Assemble all parts\ncontainer.appendChild(inputSection);\ncontainer.appendChild(outputSection);\n// Attach to shadow DOM\nthis.shadowRoot.appendChild(style);\nthis.shadowRoot.appendChild(container);\n// State\nthis.accessibilityOutput = [];\nthis.isSpeaking = false;\nthis.currentSpeakingIndex = -1;\nthis.speechSynthesis = window.speechSynthesis;\n// Bind event handlers\nthis.testButton.addEventListener('click', () => this.processHtml());\nthis.playButton.addEventListener('click', () => this.startSpeaking());\nthis.stopButton.addEventListener('click', () => this.stopSpeaking());\nthis.nonSemanticButton.addEventListener('click', () => this.insertNonSemanticExample());\nthis.semanticButton.addEventListener('click', () => this.insertSemanticExample());\n}\nprocessHtml() {\nconst html = this.htmlInput.value.trim();\nif (!html) {\nthis.showWarning('Please enter some HTML to test');\nreturn;\n}\n// Clear previous results\nthis.accessibilityOutput = [];\nthis.outputContainer.innerHTML = '';\nthis.warningsContainer.innerHTML = '';\n// Create a temporary DOM element to parse the HTML\nconst tempContainer = document.createElement('div');\ntempContainer.innerHTML = html;\n// Process the DOM tree to extract accessibility information\nthis.processNode(tempContainer, this.accessibilityOutput);\n// Render the results\nthis.renderResults();\n// Enable play button if we have results\nthis.playButton.disabled = this.accessibilityOutput.length === 0;\n}\nprocessNode(node, results) {\n// Skip comment nodes and script tags\nif (node.nodeType === Node.COMMENT_NODE ||\nnode.tagName && node.tagName.toLowerCase() === 'script') {\nreturn;\n}\n// Process text nodes with non-empty content\nif (node.nodeType === Node.TEXT_NODE) {\nconst text = node.textContent.trim();\nif (text) {\nresults.push({\ntype: 'text',\nannouncement: text,\nnode: node });\n}\nreturn;\n}\n// Process element nodes\nif (node.nodeType === Node.ELEMENT_NODE) {\n// Get accessibility attributes\nconst role = node.getAttribute('role');\nconst ariaLabel = node.getAttribute('aria-label');\nconst ariaLabelledby = node.getAttribute('aria-labelledby');\nconst ariaHidden = node.getAttribute('aria-hidden') === 'true';\nconst ariaDescribedby = node.getAttribute('aria-describedby');\n// Skip hidden elements\nif (ariaHidden) {\nreturn;\n}\n// Extract relevant info based on element type\nconst tagName = node.tagName.toLowerCase();\nlet elementInfo = { tagName };\nlet announcement = '';\nlet warnings = [];\nswitch (tagName) {\ncase 'img':\nconst alt = node.getAttribute('alt');\nif (alt === null || alt === undefined) {\nwarnings.push('Image is missing alt attribute');\nannouncement = 'Unlabelled image';\n} else if (alt === '') {\n// Empty alt is valid for decorative images\nannouncement = ''; // Screen readers skip these\n} else {\nannouncement = `Image: ${alt}`;\n}\nelementInfo = { ...elementInfo, type: 'image', alt };\nbreak;\ncase 'a':\nconst linkText = node.textContent.trim();\nconst href = node.getAttribute('href') || '';\nif (!linkText) {\nwarnings.push('Link has no text content');\n}\nif (!href) {\nwarnings.push('Link is missing href attribute');\n}\nannouncement = `Link: ${ariaLabel || linkText || 'Unlabelled link'}`;\nif (href) {\nannouncement += ` to ${href}`;\n}\nelementInfo = { ...elementInfo, type: 'link', text: linkText, href };\nbreak;\ncase 'button':\nconst buttonText = node.textContent.trim();\nif (!buttonText && !ariaLabel) {\nwarnings.push('Button has no text content or aria-label');\n}\nannouncement = `Button: ${ariaLabel || buttonText || 'Unlabelled button'}`;\nelementInfo = { ...elementInfo, type: 'button', text: buttonText };\nbreak;\ncase 'input':\nconst inputType = node.getAttribute('type') || 'text';\nconst placeholder = node.getAttribute('placeholder') || '';\nconst label = this.findLabelForInput(node);\nconst name = node.getAttribute('name') || '';\nconst required = node.hasAttribute('required');\nif (!label && !ariaLabel) {\nwarnings.push('Input field is missing an associated label or aria-label');\n}\nlet inputAnnouncement = '';\nswitch (inputType) {\ncase 'text':\ninputAnnouncement = `Text field: ${ariaLabel || label || name || 'Unlabelled field'}`;\nbreak;\ncase 'checkbox':\ninputAnnouncement = `Checkbox: ${ariaLabel || label || name || 'Unlabelled checkbox'}`;\ninputAnnouncement += node.checked ? ', checked' : ', not checked';\nbreak;\ncase 'radio':\ninputAnnouncement = `Radio button: ${ariaLabel || label || name || 'Unlabelled radio button'}`;\ninputAnnouncement += node.checked ? ', selected' : ', not selected';\nbreak;\ncase 'submit':\ninputAnnouncement = `Submit button: ${node.value || 'Submit'}`;\nbreak;\ndefault:\ninputAnnouncement = `${inputType} field: ${ariaLabel || label || name || 'Unlabelled field'}`;}\nif (required) {\ninputAnnouncement += ', required';\n}\nannouncement = inputAnnouncement;\nelementInfo = {\n...elementInfo,\ntype: 'input',\ninputType,\nlabel,\nplaceholder,\nrequired };\nbreak;\ncase 'select':\nconst selectLabel = this.findLabelForInput(node);\nif (!selectLabel && !ariaLabel) {\nwarnings.push('Select dropdown is missing an associated label or aria-label');\n}\nannouncement = `Dropdown menu: ${ariaLabel || selectLabel || 'Unlabelled dropdown'}`;\nelementInfo = { ...elementInfo, type: 'select', label: selectLabel };\nbreak;\ncase 'h1':\ncase 'h2':\ncase 'h3':\ncase 'h4':\ncase 'h5':\ncase 'h6':\nconst level = tagName.charAt(1);\nconst headingText = node.textContent.trim();\nif (!headingText) {\nwarnings.push(`Heading (${tagName}) has no text content`);\n}\nannouncement = `Heading level ${level}: ${headingText || 'Empty heading'}`;\nelementInfo = { ...elementInfo, type: 'heading', level, text: headingText };\nbreak;\ncase 'table':\nconst caption = node.querySelector('caption');\nconst captionText = caption ? caption.textContent.trim() : '';\nif (!captionText && !ariaLabel) {\nwarnings.push('Table is missing a caption or aria-label');\n}\nannouncement = `Table: ${ariaLabel || captionText || 'Unlabelled table'}`;\nelementInfo = { ...elementInfo, type: 'table', caption: captionText };\nbreak;\ncase 'figure':\nconst figcaption = node.querySelector('figcaption');\nconst figcaptionText = figcaption ? figcaption.textContent.trim() : '';\nif (!figcaptionText && !ariaLabel) {\nwarnings.push('Figure is missing a figcaption or aria-label');\n}\nannouncement = `Figure: ${ariaLabel || figcaptionText || 'Unlabelled figure'}`;\nelementInfo = { ...elementInfo, type: 'figure', caption: figcaptionText };\n// Process children separately to maintain context\nconst children = Array.from(node.children);\nchildren.forEach(child => this.processNode(child, results));\n// Skip processing children again\nreturn;\ncase 'ul':\nannouncement = `Unordered list with ${node.children.length} items`;\nelementInfo = { ...elementInfo, type: 'list', listType: 'unordered' };\nbreak;\ncase 'ol':\nannouncement = `Ordered list with ${node.children.length} items`;\nelementInfo = { ...elementInfo, type: 'list', listType: 'ordered' };\nbreak;\ncase 'li':\nconst listItemText = node.textContent.trim();\nlet prefix = '';\nif (node.parentElement) {\nif (node.parentElement.tagName.toLowerCase() === 'ol') {\n// Find index of this li element among its siblings\nconst siblings = Array.from(node.parentElement.children);\nconst index = siblings.indexOf(node) + 1;\nprefix = `Item ${index}: `;\n} else {\nprefix = 'Bullet: ';\n}\n}\nannouncement = `${prefix}${listItemText}`;\nelementInfo = { ...elementInfo, type: 'listItem', text: listItemText };\nbreak;\ncase 'form':\nannouncement = `Form: ${ariaLabel || 'Unlabelled form'}`;\nelementInfo = { ...elementInfo, type: 'form' };\nbreak;\n// Add more element types as needed\ndefault:\n// For other elements, only process if they have meaningful accessibility attributes\nif (ariaLabel) {\nannouncement = ariaLabel;\nelementInfo = { ...elementInfo, type: 'custom', role };\n} else if (role) {\nannouncement = `${role}: ${node.textContent.trim()}`;\nelementInfo = { ...elementInfo, type: 'custom', role };\n}}\n// Add result if we have an announcement\nif (announcement) {\nresults.push({\n...elementInfo,\nannouncement,\nwarnings,\nnode });\n}\n// Recursively process child nodes\nconst children = Array.from(node.childNodes);\nchildren.forEach(child => this.processNode(child, results));\n}\n}\nfindLabelForInput(input) {\n// Check for label with matching 'for' attribute\nif (input.id) {\nconst label = document.querySelector(`label[for=\"${input.id}\"]`);\nif (label) {\nreturn label.textContent.trim();\n}\n}\n// Check if input is wrapped in a label\nlet parent = input.parentElement;\nwhile (parent) {\nif (parent.tagName.toLowerCase() === 'label') {\n// Get text content excluding the input itself\nconst clone = parent.cloneNode(true);\nconst inputInClone = clone.querySelector('input, select, textarea');\nif (inputInClone) {\ninputInClone.remove();\n}\nreturn clone.textContent.trim();\n}\nparent = parent.parentElement;\n}\nreturn '';\n}\nrenderResults() {\nif (this.accessibilityOutput.length === 0) {\nthis.outputContainer.innerHTML = '<p>No accessibility information found or HTML is empty.</p>';\nreturn;\n}\nconst resultElement = document.createElement('div');\nresultElement.className = 'a11y-result';\nconst resultHeader = document.createElement('div');\nresultHeader.className = 'a11y-result-header';\nresultHeader.textContent = 'Screen Reader Navigation Sequence';\nresultElement.appendChild(resultHeader);\nconst resultContent = document.createElement('div');\nresultContent.className = 'a11y-result-content';\n// Create elements for each result\nthis.accessibilityOutput.forEach((item, index) => {\nconst element = document.createElement('div');\nelement.className = 'a11y-element';\nelement.dataset.index = index;\nconst elementType = document.createElement('div');\nelementType.className = 'a11y-element-type';\nelementType.textContent = item.type ?\nitem.type.charAt(0).toUpperCase() + item.type.slice(1) :\n'Element';\nelement.appendChild(elementType);\nconst announcement = document.createElement('div');\nannouncement.className = 'a11y-element-announcement';\nannouncement.textContent = item.announcement;\nelement.appendChild(announcement);\nresultContent.appendChild(element);\n// Add warnings to the warnings container\nif (item.warnings && item.warnings.length > 0) {\nitem.warnings.forEach(warning => {\nthis.showWarning(warning, item.type || 'element');\n});\n}\n});\nresultElement.appendChild(resultContent);\nthis.outputContainer.appendChild(resultElement);\n}\nshowWarning(message, context = '') {\nconst warning = document.createElement('div');\nwarning.className = 'a11y-warning';\nwarning.textContent = context ? `${context}: ${message}` : message;\nthis.warningsContainer.appendChild(warning);\n}\nstartSpeaking() {\nif (this.isSpeaking) return;\nthis.isSpeaking = true;\nthis.currentSpeakingIndex = 0;\nthis.playButton.disabled = true;\nthis.stopButton.disabled = false;\nthis.speakCurrent();\n}\nspeakCurrent() {\nif (!this.isSpeaking ||\nthis.currentSpeakingIndex >= this.accessibilityOutput.length) {\nthis.stopSpeaking();\nreturn;\n}\n// Highlight current element\nconst elements = this.shadowRoot.querySelectorAll('.a11y-element');\nelements.forEach(el => el.classList.remove('speaking'));\nconst currentElement = this.shadowRoot.querySelector(`.a11y-element[data-index=\"${this.currentSpeakingIndex}\"]`);\nif (currentElement) {\ncurrentElement.classList.add('speaking');\ncurrentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });\n}\n// Get current announcement\nconst item = this.accessibilityOutput[this.currentSpeakingIndex];\nconst text = item.announcement;\n// Use speech synthesis to speak the text\nconst utterance = new SpeechSynthesisUtterance(text);\nutterance.onend = () => {\n// Move to next item after speaking completes\nthis.currentSpeakingIndex++;\nsetTimeout(() => this.speakCurrent(), 500); // Small delay between items\n};\nthis.speechSynthesis.speak(utterance);\n}\nstopSpeaking() {\nthis.isSpeaking = false;\nthis.speechSynthesis.cancel();\nthis.playButton.disabled = false;\nthis.stopButton.disabled = true;\n// Remove highlighting\nconst elements = this.shadowRoot.querySelectorAll('.a11y-element');\nelements.forEach(el => el.classList.remove('speaking'));\n}\ninsertNonSemanticExample() {\nthis.htmlInput.value = this.getNonSemanticHtml();\n// Auto-process the example\nthis.processHtml();\n}\ninsertSemanticExample() {\nthis.htmlInput.value = this.getSemanticHtml();\n// Auto-process the example\nthis.processHtml();\n}\ngetNonSemanticHtml() {\nreturn `<div>\n<div style=\"font-size: 24px; font-weight: bold;\">My Online Store</div>\n<div style=\"background-color: #f0f0f0; padding: 10px;\">\n<div style=\"font-weight: bold;\">Products</div>\n<div>Home</div>\n<div>About</div>\n<div>Contact</div>\n</div>\n<div>\n<div style=\"font-size: 20px; font-weight: bold;\">Featured Products</div>\n<div style=\"display: flex; flex-wrap: wrap;\">\n<div style=\"border: 1px solid #ddd; margin: 10px; padding: 15px; width: 200px;\">\n<img src=\"https://via.placeholder.com/150\" alt=\"\">\n<div style=\"font-weight: bold;\">Product 1</div>\n<div>$19.99</div>\n<div style=\"color: #666;\">★★★★☆ (4.2/5)</div>\n<div style=\"background-color: #0066cc; color: white; padding: 5px; text-align: center; cursor: pointer;\">Add to Cart</div>\n</div>\n<div style=\"border: 1px solid #ddd; margin: 10px; padding: 15px; width: 200px;\">\n<img src=\"https://via.placeholder.com/150\">\n<div style=\"font-weight: bold;\">Product 2</div>\n<div>$24.99</div>\n<div style=\"color: #666;\">★★★★★ (4.9/5)</div>\n<div style=\"background-color: #0066cc; color: white; padding: 5px; text-align: center; cursor: pointer;\">Add to Cart</div>\n</div>\n</div>\n</div>\n<div style=\"margin-top: 20px; background-color: #333; color: white; padding: 10px; text-align: center;\">\n© 2023 My Online Store. All rights reserved.\n</div>\n</div>`;\n}\ngetSemanticHtml() {\nreturn `<header>\n<h1>My Online Store</h1>\n<nav aria-label=\"Main Navigation\">\n<ul>\n<li><a href=\"#products\" aria-current=\"page\">Products</a></li>\n<li><a href=\"/\">Home</a></li>\n<li><a href=\"/about\">About</a></li>\n<li><a href=\"/contact\">Contact</a></li>\n</ul>\n</nav>\n</header>\n<main>\n<section aria-labelledby=\"featured-heading\">\n<h2 id=\"featured-heading\">Featured Products</h2>\n<div class=\"product-grid\">\n<article class=\"product\">\n<img src=\"https://via.placeholder.com/150\" alt=\"Featured product 1 thumbnail\" width=\"150\" height=\"150\">\n<h3>Product 1</h3>\n<p class=\"price\">$19.99</p>\n<div class=\"ratings\" aria-label=\"Rated 4.2 out of 5 stars\">★★★★☆ (4.2/5)</div>\n<button type=\"button\" aria-label=\"Add Product 1 to cart\">Add to Cart</button>\n</article>\n<article class=\"product\">\n<img src=\"https://via.placeholder.com/150\" alt=\"Featured product 2 thumbnail\" width=\"150\" height=\"150\">\n<h3>Product 2</h3>\n<p class=\"price\">$24.99</p>\n<div class=\"ratings\" aria-label=\"Rated 4.9 out of 5 stars\">★★★★★ (4.9/5)</div>\n<button type=\"button\" aria-label=\"Add Product 2 to cart\">Add to Cart</button>\n</article>\n</div>\n</section>\n</main>\n<footer>\n<p>© 2023 My Online Store. All rights reserved.</p>\n</footer>`;\n}}\n// Register the custom element\ncustomElements.define('a11y-tester', AccessibilityTester);\nScreen Reader Simulator on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\n4. Testing with NVDA\nNVDA (NonVisual Desktop Access) is a free screen reader for Windows. Download it from nvaccess.org (https://www.nvaccess.org/). After installation, you'll hear a startup sound and voice feedback will begin. Use the NVDA key (usually Insert) combined with other keys to give it commands. Press NVDA+Space to switch between browse mode (reading the page) and focus mode (typing into a control).\nPay attention to how NVDA announces your interface elements. Ensure all interactive elements are properly labeled and keyboard-accessible. Test different scenarios: form completion, menu navigation, and custom component interaction. In browse mode, single keys jump around the page: H to the next heading and D to the next landmark. Listen for proper announcements of alerts, verify that dynamic content changes are announced, and check that error messages are accessible.\nThis is the closest you can get to Sarah's experience without her: turn off your monitor, or look away, and try to finish the component's task by ear and keyboard.\nCommon Accessibility Pitfalls in Web Components\nEven with thorough testing, certain accessibility issues frequently emerge in Web Component development. By understanding these common pitfalls, you can address them proactively in your implementation.\n1. Shadow DOM Isolation Issues\nProblem: ARIA relationships that cross shadow boundaries don't work as expected.\nSolution: Keep related elements within the same shadow root or use alternative techniques like aria-live regions for updates.\n2. Forgetting About Non-Mouse Users\nProblem: Components only work with mouse events (click, hover).\nSolution: Start from a real <button>. It is focusable and fires click for Enter and Space, so one handler covers mouse, keyboard and touch. If you truly cannot use a button, you have to rebuild all of that by hand.\n// Bad example - a <div> only reacts to the mouse.\n// It cannot be reached with Tab, and Enter or Space do nothing.\nconst fake = document.createElement('div');\nfake.textContent = 'Save';\nfake.addEventListener('click', handleAction);\n// Good example - a <button> needs no extra code\nconst button = document.createElement('button');\nbutton.textContent = 'Save';\nbutton.addEventListener('click', handleAction);\n// Last resort - keep the <div>, but add everything a button gives you\nfake.setAttribute('role', 'button');\nfake.setAttribute('tabindex', '0');\nfake.addEventListener('keydown', e => {\nif (e.key === 'Enter' || e.key === ' ') {\ne.preventDefault();\nhandleAction();\n}\n});\nNow try it yourself. The starter below is a \"button\" built from a <div>, with the extra code it needs.\nHTML\n<div id=\"save\" role=\"button\" tabindex=\"0\">Save</div>\n<p id=\"status\">Saved 0 times.</p>\nCSS\nbody { font-family: sans-serif; }\n#save, button { display: inline-block; padding: 8px 16px; border: 2px solid #333; border-radius: 4px; cursor: pointer; }\n#save:focus-visible, button:focus-visible { outline: 3px solid blue; outline-offset: 2px; }\nJavaScript\nlet count = 0;\nconst statusEl = document.getElementById('status');\nfunction save() {\ncount++;\nstatusEl.textContent = `Saved ${count} times.`;\n}\nconst control = document.getElementById('save');\ncontrol.addEventListener('click', save);\n// TODO: once #save is a real <button>, delete this whole block\ncontrol.addEventListener('keydown', (e) => {\nif (e.key === 'Enter' || e.key === ' ') {\ne.preventDefault();\nsave();\n}\n});\nChallenge: delete code by using a real button on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\nPress Tab, then Enter and Space, and watch the counter. Then: in the HTML tab, replace the <div id=\"save\" role=\"button\" tabindex=\"0\">Save</div> with <button id=\"save\">Save</button>, and in the JavaScript tab delete the keydown block. Count the lines you deleted. The expected result is the same behavior (Tab reaches it, Enter and Space each add one to the counter, a click adds one) with the whole 6-line keydown block gone, plus the role and tabindex attributes.\nTip:\nNothing visibly breaks if you leave the keydown block in place, because its preventDefault() stops the button's own click for those keys, so the count still goes up by one. That is the lesson: the block is now dead weight. To see why the <div> needed it, change the HTML to a plain <div id=\"save\">Save</div> and press Tab: focus never lands on it, and Enter does nothing.\n3. Poor Focus Management\nProblem: Focus gets lost during dynamic updates or when elements are removed.\nSolution: Always manage focus explicitly, especially when showing/hiding content.\n4. Insufficient Color Contrast\nProblem: Text or UI controls don't have sufficient contrast with their backgrounds.\nSolution: Follow WCAG guidelines for contrast ratios (4.5:1 for normal text, 3:1 for large text).\nNow that we've identified potential problems, let's consolidate what we've learned into actionable best practices that will guide your development process from ideation through implementation.\nBest Practices\nUse Native Elements When Possible: Use native HTML elements with inherent accessibility support.\nProvide Accessible Names: Ensure all interactive elements have accessible names using aria-label, aria-labelledby, or semantic HTML.\nEnsure Focus Visibility: Maintain visible focus indicators for keyboard and assistive technology users.\nHandle Dynamic Content Carefully: When updating content, manage focus appropriately and announce changes.\nTest Regularly: Incorporate accessibility testing into your development process.\nDocument Accessibility Features: Include accessibility information in your component documentation.\nThese best practices come together most effectively when applied to real-world scenarios. Let us look at a larger example that brings several accessibility features together into a practical, commonly-used component:\nReal-World Example: An Accessible Search Component\nLet's combine everything we've learned into a practical example:\nHTML\n<accessible-search></accessible-search>\nCSS\n/* Simple Dark Mode CSS */\n/* Default light mode */\n:root {\n--bg-color: #ffffff;\n--text-color: #333333;\n}\n/* Dark mode detection */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n}\n}\n/* Basic styling */\nbody {\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\nsans-serif;\nline-height: 1.5;\nmargin: 0;\npadding: 0;\ntransition: background-color 0.3s ease, color 0.3s ease;\ndisplay: flex;\nflex-direction: column;\nalign-items: center;\njustify-content: center;\nmin-height: 100vh;\ntext-align: center;\n}\nJavaScript\nclass AccessibleSearch extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.results = [];\nthis.render();\n}\nconnectedCallback() {\n// Get DOM elements\nconst container = this.shadowRoot.querySelector('.search-container');\nconst input = this.shadowRoot.querySelector('input');\nconst button = this.shadowRoot.querySelector('button');\nconst results = this.shadowRoot.querySelector('.results');\nconst themeToggle = this.shadowRoot.querySelector('#theme-toggle');\n// Example data - in a real component, this might come from an API\nconst sampleData = [\n'JavaScript', 'TypeScript', 'HTML', 'CSS', 'React',\n'Angular', 'Vue', 'Svelte', 'Web Components', 'Accessibility'];\n// Check for system preference initially\nconst prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;\nif (prefersDarkMode) {\ncontainer.classList.add('dark-theme');\n}\n// Listen for system preference changes\nwindow.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {\nif (e.matches) {\ncontainer.classList.add('dark-theme');\n} else {\ncontainer.classList.remove('dark-theme');\n}\n});\n// Theme toggle functionality\nthemeToggle.addEventListener('click', () => {\ncontainer.classList.toggle('dark-theme');\n// Announce theme change to screen readers\nconst isDarkTheme = container.classList.contains('dark-theme');\nconst announcement = this.shadowRoot.querySelector('.theme-announcement');\nannouncement.textContent = `Theme changed to ${isDarkTheme ? 'dark' : 'light'} mode`;\n});\n// Search input handling\ninput.addEventListener('input', () => {\nconst query = input.value.toLowerCase();\nif (query.length < 2) {\nthis.results = [];\n} else {\nthis.results = sampleData.filter((item) =>\nitem.toLowerCase().includes(query));\n}\nthis.updateResults();\n});\n// Search button click\nbutton.addEventListener('click', () => {\nthis.performSearch();\n});\n// Handle keyboard input\ninput.addEventListener('keydown', e => {\nif (e.key === 'Enter') {\ne.preventDefault();\nthis.performSearch();\n}\n// Allow keyboard navigation through results\nif (e.key === 'ArrowDown' && this.results.length > 0) {\ne.preventDefault();\nthis.shadowRoot.querySelector('.result-item').focus();\n}\n});\n// Delegate event listener for result items\nresults.addEventListener('keydown', e => {\nif (!e.target.classList.contains('result-item')) return;\nconst resultItems = Array.from(this.shadowRoot.querySelectorAll('.result-item'));\nconst currentIndex = resultItems.indexOf(e.target);\nif (e.key === 'ArrowDown' && currentIndex < resultItems.length - 1) {\ne.preventDefault();\nresultItems[currentIndex + 1].focus();\n}\nif (e.key === 'ArrowUp') {\ne.preventDefault();\nif (currentIndex === 0) {\ninput.focus();\n} else {\nresultItems[currentIndex - 1].focus();\n}\n}\nif (e.key === 'Enter') {\ne.preventDefault();\nthis.selectResult(e.target.textContent);\n}\nif (e.key === 'Escape') {\ne.preventDefault();\nthis.results = [];\nthis.updateResults();\ninput.focus();\n}\n});\n// Handle result item clicks\nresults.addEventListener('click', e => {\nif (e.target.classList.contains('result-item')) {\nthis.selectResult(e.target.textContent);\n}\n});\n}\nperformSearch() {\nconst announcement = this.shadowRoot.querySelector('.search-announcement');\nconst resultCount = this.results.length;\nannouncement.textContent = `Found ${resultCount} result${resultCount === 1 ? '' : 's'}`;\n}\nselectResult(text) {\nconst input = this.shadowRoot.querySelector('input');\ninput.value = text;\nthis.results = [];\nthis.updateResults();\ninput.focus();\n// Announce selection to screen readers\nconst announcement = this.shadowRoot.querySelector('.search-announcement');\nannouncement.textContent = `Selected ${text}`;\n}\nupdateResults() {\nconst resultsContainer = this.shadowRoot.querySelector('.results');\nif (this.results.length === 0) {\nresultsContainer.innerHTML = '';\nresultsContainer.setAttribute('aria-hidden', 'true');\nreturn;\n}\nresultsContainer.removeAttribute('aria-hidden');\nresultsContainer.innerHTML = this.results.map((result) =>\n`<button class=\"result-item\" tabindex=\"0\">${result}</button>`).\njoin('');\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n/* Base styles */\n* {\nbox-sizing: border-box;\nmargin: 0;\npadding: 0;\n}\n/* Container styles */\n.search-container {\nposition: relative;\nwidth: 100%;\nmax-width: 500px;\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\npadding: 20px;\nborder-radius: 8px;\nbackground-color: #ffffff;\ncolor: #333333;\ntransition: all 0.3s ease;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n}\n/* Dark theme */\n.search-container.dark-theme {\nbackground-color: #1e1e1e;\ncolor: #e0e0e0;\nbox-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n}\n/* Header with search label and theme toggle */\n.search-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nmargin-bottom: 12px;\n}\nlabel {\nfont-size: 16px;\nfont-weight: 500;\n}\n.dark-theme label {\ncolor: #f0f0f0;\n}\n/* Search input container */\n.search-input {\ndisplay: flex;\nwidth: 100%;\nmargin-bottom: 8px;\n}\ninput {\nflex: 1;\npadding: 10px 12px;\nborder: 2px solid #ccc;\nborder-radius: 4px 0 0 4px;\nfont-size: 16px;\nbackground-color: #ffffff;\ncolor: #333333;\ntransition: all 0.2s ease;\n}\n.dark-theme input {\nbackground-color: #2d2d2d;\nborder-color: #444;\ncolor: #e0e0e0;\n}\ninput:focus {\noutline: none;\nborder-color: #4a90e2;\nbox-shadow: 0 0 0 1px #4a90e2;\n}\n.dark-theme input:focus {\nborder-color: #5a9de4;\nbox-shadow: 0 0 0 1px #5a9de4;\n}\n/* Button styles */\nbutton {\npadding: 10px 16px;\nfont-size: 16px;\ncursor: pointer;\nbackground-color: #4a90e2;\ncolor: white;\nborder: none;\ntransition: all 0.2s ease;\n}\n.dark-theme button {\nbackground-color: #3a7bc2;\n}\nbutton:hover {\nbackground-color: #3a7bc2;\n}\n.dark-theme button:hover {\nbackground-color: #2a6cb2;\n}\nbutton:focus-visible {\noutline: 3px solid #90c2ff;\noutline-offset: 2px;\nposition: relative;\nz-index: 1;\n}\n.dark-theme button:focus-visible {\noutline-color: #5a9de4;\n}\n/* Search button */\n#search-button {\nborder-radius: 0 4px 4px 0;\n}\n/* Theme toggle button */\n#theme-toggle {\nbackground-color: #6c757d;\nborder-radius: 4px;\nmargin-left: 10px;\npadding: 8px;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n.dark-theme #theme-toggle {\nbackground-color: #495057;\n}\n#theme-toggle:hover {\nbackground-color: #5a6268;\n}\n.dark-theme #theme-toggle:hover {\nbackground-color: #3d4348;\n}\n/* Theme icon */\n.theme-icon {\nwidth: 20px;\nheight: 20px;\nfill: white;\n}\n/* Results container */\n.results {\nposition: absolute;\ntop: 100%;\nleft: 20px;\nright: 20px;\nz-index: 10;\nmax-height: 300px;\noverflow-y: auto;\nbackground: white;\nborder: 1px solid #ccc;\nborder-radius: 0 0 4px 4px;\nbox-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\nmargin-top: -8px;\n}\n.dark-theme .results {\nbackground-color: #2d2d2d;\nborder-color: #444;\nbox-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);\n}\n/* Result items */\n.result-item {\ndisplay: block;\nwidth: 100%;\ntext-align: left;\npadding: 10px 16px;\nbackground: none;\nborder: none;\nborder-bottom: 1px solid #eee;\ncursor: pointer;\ncolor: #333;\ntransition: background-color 0.2s;\n}\n.dark-theme .result-item {\nborder-bottom-color: #444;\ncolor: #e0e0e0;\n}\n.result-item:last-child {\nborder-bottom: none;\n}\n.result-item:hover, .result-item:focus {\nbackground-color: #f5f5f5;\n}\n.dark-theme .result-item:hover, .dark-theme .result-item:focus {\nbackground-color: #3d3d3d;\n}\n/* Screen reader only elements */\n.sr-only {\nposition: absolute;\nwidth: 1px;\nheight: 1px;\npadding: 0;\nmargin: -1px;\noverflow: hidden;\nclip: rect(0, 0, 0, 0);\nwhite-space: nowrap;\nborder-width: 0;\n}\n/* For high contrast mode support */\n@media (forced-colors: active) {\ninput:focus, button:focus-visible {\noutline: 3px solid CanvasText;\n}\n.result-item:focus {\noutline: 3px solid CanvasText;\noutline-offset: -3px;\n}\n}\n</style>\n<div class=\"search-container\">\n<div class=\"search-header\">\n<label id=\"search-label\" for=\"search-input\">Search for web technologies</label>\n<button id=\"theme-toggle\" aria-label=\"Toggle dark theme\">\n<svg class=\"theme-icon\" viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n<path d=\"M12,22 C17.5228475,22 22,17.5228475 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,17.5228475 6.4771525,22 12,22 Z M12,20 L12,4 C16.418278,4 20,7.581722 20,12 C20,16.418278 16.418278,20 12,20 Z\"></path>\n</svg>\n</button>\n</div>\n<div class=\"search-input\">\n<input\nid=\"search-input\"\ntype=\"text\"\naria-labelledby=\"search-label\"\naria-autocomplete=\"list\"\naria-controls=\"search-results\"\naria-expanded=\"false\"\nrole=\"combobox\"\nautocomplete=\"off\"\nplaceholder=\"Type to search...\"\n/>\n<button id=\"search-button\" aria-label=\"Search\">\nSearch\n</button>\n</div>\n<div\nid=\"search-results\"\nclass=\"results\"\nrole=\"listbox\"\naria-label=\"Search suggestions\"\naria-hidden=\"true\"\n></div>\n<!-- Live regions for screen readers -->\n<div class=\"search-announcement sr-only\" aria-live=\"polite\"></div>\n<div class=\"theme-announcement sr-only\" aria-live=\"polite\"></div>\n<div id=\"instructions\" class=\"sr-only\">\nUse up and down arrow keys to navigate results. Press Enter to select a result. Press Escape to clear results.\n</div>\n</div>\n`;\n}}\ncustomElements.define('accessible-search', AccessibleSearch);\nAccessible Search Web Component Example on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/).\n<details>\n<summary>Accessible search JavaScript · 222 lines</summary>\nclass AccessibleSearch extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\nthis.results = [];\nthis.render();\n}\nconnectedCallback() {\nconst input = this.shadowRoot.querySelector('input');\nconst button = this.shadowRoot.querySelector('button');\nconst results = this.shadowRoot.querySelector('.results');\n// Example data - in a real component, this might come from an API\nconst sampleData = [\n'JavaScript', 'TypeScript', 'HTML', 'CSS', 'React',\n'Angular', 'Vue', 'Svelte', 'Web Components', 'Accessibility'\n];\ninput.addEventListener('input', () => {\nconst query = input.value.toLowerCase();\nif (query.length < 2) {\nthis.results = [];\n} else {\nthis.results = sampleData.filter(item =>\nitem.toLowerCase().includes(query)\n);\n}\nthis.updateResults();\n});\nbutton.addEventListener('click', () => {\nthis.performSearch();\n});\ninput.addEventListener('keydown', e => {\nif (e.key === 'Enter') {\ne.preventDefault();\nthis.performSearch();\n}\n// Allow keyboard navigation through results\nif (e.key === 'ArrowDown' && this.results.length > 0) {\ne.preventDefault();\nthis.shadowRoot.querySelector('.result-item').focus();\n}\n});\n// Delegate event listener for result items\nresults.addEventListener('keydown', e => {\nif (!e.target.classList.contains('result-item')) return;\nconst resultItems = Array.from(this.shadowRoot.querySelectorAll('.result-item'));\nconst currentIndex = resultItems.indexOf(e.target);\nif (e.key === 'ArrowDown' && currentIndex < resultItems.length - 1) {\ne.preventDefault();\nresultItems[currentIndex + 1].focus();\n}\nif (e.key === 'ArrowUp') {\ne.preventDefault();\nif (currentIndex === 0) {\ninput.focus();\n} else {\nresultItems[currentIndex - 1].focus();\n}\n}\nif (e.key === 'Enter') {\ne.preventDefault();\nthis.selectResult(e.target.textContent);\n}\n});\n}\nperformSearch() {\nconst announcement = this.shadowRoot.querySelector('.screen-reader-announcement');\nconst resultCount = this.results.length;\nannouncement.textContent = `Found ${resultCount} result${resultCount === 1 ? '' : 's'}`;\n}\nselectResult(text) {\nconst input = this.shadowRoot.querySelector('input');\ninput.value = text;\nthis.results = [];\nthis.updateResults();\ninput.focus();\n// Announce selection to screen readers\nconst announcement = this.shadowRoot.querySelector('.screen-reader-announcement');\nannouncement.textContent = `Selected ${text}`;\n}\nupdateResults() {\nconst resultsContainer = this.shadowRoot.querySelector('.results');\nif (this.results.length === 0) {\nresultsContainer.innerHTML = '';\nresultsContainer.setAttribute('aria-hidden', 'true');\nreturn;\n}\nresultsContainer.removeAttribute('aria-hidden');\nresultsContainer.innerHTML = this.results.map(result =>\n`<button class=\"result-item\" tabindex=\"0\">${result}</button>`\n).join('');\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n.search-container {\nposition: relative;\nwidth: 300px;\n}\n.search-input {\ndisplay: flex;\n}\ninput {\nflex: 1;\npadding: 8px;\nborder: 2px solid #ccc;\nborder-radius: 4px 0 0 4px;\nfont-size: 16px;\n}\ninput:focus {\noutline: none;\nborder-color: blue;\n}\nbutton {\npadding: 8px 16px;\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nborder-radius: 0 4px 4px 0;\ncursor: pointer;\n}\nbutton:focus-visible {\noutline: 3px solid blue;\noutline-offset: 2px;\n}\n.results {\nposition: absolute;\ntop: 100%;\nleft: 0;\nright: 0;\nz-index: 10;\nbackground: white;\nborder: 1px solid #ccc;\nborder-top: none;\nmax-height: 200px;\noverflow-y: auto;\n}\n.result-item {\ndisplay: block;\nwidth: 100%;\ntext-align: left;\npadding: 8px;\nbackground: none;\nborder: none;\nborder-bottom: 1px solid #eee;\ncursor: pointer;\n}\n.result-item:hover, .result-item:focus {\nbackground-color: #f1f1f1;\n}\n.screen-reader-only {\nposition: absolute;\nwidth: 1px;\nheight: 1px;\npadding: 0;\nmargin: -1px;\noverflow: hidden;\nclip: rect(0, 0, 0, 0);\nwhite-space: nowrap;\nborder-width: 0;\n}\n</style>\n<div class=\"search-container\">\n<label id=\"search-label\" for=\"search-input\">Search</label>\n<div class=\"search-input\">\n<input\nid=\"search-input\"\ntype=\"text\"\naria-labelledby=\"search-label\"\naria-autocomplete=\"list\"\naria-controls=\"search-results\"\nautocomplete=\"off\"\n/>\n<button aria-label=\"Search\">Go</button>\n</div>\n<div\nid=\"search-results\"\nclass=\"results\"\nrole=\"listbox\"\naria-label=\"Search suggestions\"\naria-hidden=\"true\"\n></div>\n<div\nclass=\"screen-reader-announcement screen-reader-only\"\naria-live=\"polite\"\n></div>\n</div>\n`;\n}\n}\ncustomElements.define('accessible-search', AccessibleSearch);\n</details>\nKey accessibility features:\nProper labeling of all elements\nKeyboard navigation through results with arrow keys\nProper ARIA roles and relationships\nLive region announcements for search results and selections\nFull keyboard functionality without requiring a mouse\nAs this search component demonstrates, accessible Web Components require attention to multiple factors—from semantic structure to keyboard interactions and dynamic announcements. By applying these techniques to your own components, you'll create experiences that work for everyone.\nConclusion\nCreating accessible Web Components isn't just about compliance—it's about creating a better web for everyone. By incorporating these practices from the start, you'll build components that more people can use.\nRemember these key principles:\nUse semantic HTML whenever possible\nImplement proper keyboard support\nManage focus explicitly\nAnnounce dynamic changes with ARIA live regions\nTest thoroughly with both automated and manual methods\nBy building accessibility into your Web Components from the ground up, you're not just helping users with disabilities—you're creating a better experience for everyone.\nResources\nWeb Accessibility Initiative (WAI) (https://www.w3.org/WAI/)\nMDN Web Components Guide (https://developer.mozilla.org/en-US/docs/Web/Web_Components)\nARIA Authoring Practices Guide (https://www.w3.org/WAI/ARIA/apg/)\nGitHub repository containing these examples (https://github.com/mnichols08/WebComponentAccesibility/)\nDemo Page from the repository (https://mnichols08.github.io/WebComponentAccesibility/)\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/accessibility-in-web-components/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-03-30T16:00:28Z",
      "id": "https://mnixblog.netlify.app/accessibility-in-web-components/",
      "image": "https://mnixblog.netlify.app/images/accessibility-in-web-components/cover-v4.jpg",
      "summary": "Build custom elements that say what they are, work from the keyboard, and announce changes to screen reader users.",
      "tags": [
        "JavaScript",
        "Web Components",
        "Accessibility",
        "Beginners"
      ],
      "title": "Accessibility in Web Components",
      "url": "https://mnixblog.netlify.app/accessibility-in-web-components/"
    },
    {
      "content_html": "<p>A custom element is a tag we define, and the browser tells it when something happens to it: it appeared on the page, it was removed, or one of its attributes changed. This post covers those moments (the lifecycle callbacks), then how components pass messages to each other.</p>\n<p><strong>Quick recap if you are joining here:</strong> a custom element is a class that <code>extends HTMLElement</code>, registered with <code>customElements.define('my-tag', MyClass)</code>. Shadow DOM gives it a private inside, so the page's CSS cannot leak in or out. To keep the short snippets short, most of them write straight into the element with <code>innerHTML</code> and no shadow root; the live demos use Shadow DOM. (Only put text you trust into <code>innerHTML</code>.)</p>\n<p>We'll cover two things:</p>\n<ol>\n<li><strong>Lifecycle callbacks</strong>: the four hooks the browser calls on your element</li>\n<li><strong>Component communication</strong>: how components share data and news</li>\n</ol>\n<hr />\n<h2 id=\"lifecycle-callbacks-the-heartbeat-of-your-components\">Lifecycle Callbacks: The Heartbeat of Your Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#lifecycle-callbacks-the-heartbeat-of-your-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>Imagine your Web Component as a living entity with a clear lifecycle: it's born, grows, changes, and eventually leaves the page. Each of these stages triggers specific callback methods that you can tap into:</p>\n<h3 id=\"1-connectedcallback-the-birthday-party\">1. <code>connectedCallback()</code>: The Birthday Party<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#1-connectedcallback-the-birthday-party\" aria-label=\"Link to this section\">#</a></h3>\n<p>When your component joins the DOM, it's time to celebrate! This is where initialization happens:</p>\n<pre><code class=\"language-javascript\">class BirthdayComponent extends HTMLElement {\n  connectedCallback() {\n    console.log('🎉 I\\'m alive! Time to party!');\n    this.innerHTML = `\n      &lt;div class=\"birthday-card\"&gt;\n        &lt;h2&gt;Hello World!&lt;/h2&gt;\n        &lt;p&gt;I was born at ${new Date().toLocaleTimeString()}&lt;/p&gt;\n      &lt;/div&gt;\n    `;\n    // Start any necessary timers, fetch data, etc.\n  }\n}\n\ncustomElements.define('birthday-card', BirthdayComponent);\n</code></pre>\n<p><strong>Try it yourself:</strong> Add <code>&lt;birthday-card&gt;&lt;/birthday-card&gt;</code> to your HTML and watch it come to life in the browser console!</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Connected Callback Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;tab-content active&quot; id=&quot;birthday-tab&quot; role=&quot;tabpanel&quot; aria-labelledby=&quot;tab-birthday&quot;&gt;\n        &lt;div class=&quot;controls&quot;&gt;\n          &lt;button id=&quot;add-birthday&quot;&gt;Add Birthday Component&lt;/button&gt;\n          &lt;button id=&quot;remove-all-birthday&quot; class=&quot;remove&quot;&gt;Remove All Birthday Components&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div id=&quot;birthday-container&quot; class=&quot;components-container&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styles */\n:root {\n  /* Light mode variables */\n  --bg-color: #f5f5f5;\n  --text-color: #333;\n  --card-bg: #fff;\n  --shadow-color: rgba(0,0,0,0.1);\n  --border-color: #eee;\n  --highlight-color: #f0f0f0;\n  \n  /* Button colors */\n  --btn-primary: #4CAF50;\n  --btn-primary-hover: #45a049;\n  --btn-cleanup: #2196F3;\n  --btn-cleanup-hover: #0b7dda;\n  --btn-mood: #9C27B0;\n  --btn-mood-hover: #7B1FA2;\n  --btn-remove: #f44336;\n  --btn-remove-hover: #d32f2f;\n  --btn-adopted: #8bc34a;\n  --btn-adopted-hover: #7cb342;\n  \n  /* Console colors */\n  --console-bg: #333;\n  --console-text: #fff;\n  --console-birthday: #FF9800;\n  --console-cleanup: #2196F3;\n  --console-mood: #E91E63;\n  --console-adopted: #8bc34a;\n  --console-communication: #4CAF50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-color: #121212;\n    --text-color: #e0e0e0;\n    --card-bg: #1e1e1e;\n    --shadow-color: rgba(0,0,0,0.3);\n    --border-color: #333;\n    --highlight-color: #2c2c2c;\n    \n    /* Adjust button colors for better contrast in dark mode */\n    --btn-primary: #5CDF60;\n    --btn-primary-hover: #4cd050;\n    --btn-cleanup: #42a6ff;\n    --btn-cleanup-hover: #3b95e5;\n    --btn-mood: #bb4fd0;\n    --btn-mood-hover: #a53fb8;\n    --btn-remove: #ff6b60;\n    --btn-remove-hover: #e55c52;\n    --btn-adopted: #9bd45a;\n    --btn-adopted-hover: #8bc34a;\n    \n    /* Console colors (keep high contrast) */\n    --console-bg: #1a1a1a;\n    --console-text: #f0f0f0;\n  }\n}\n\n/* High contrast mode styles for accessibility testing */\n\n/* Debug controls styling */\n\nbody {\n  font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.controls {\n  margin-bottom: 20px;\n  padding: 15px;\n  background-color: var(--card-bg);\n  border-radius: 8px;\n  box-shadow: 0 2px 4px var(--shadow-color);\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n}\n\nbutton {\n  background-color: var(--btn-primary);\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.3s, box-shadow 0.3s;\n  position: relative;\n}\n\nbutton:focus-visible {\n  outline: 3px solid #4d90fe;\n  outline-offset: 2px;\n}\n\nbutton:hover {\n  background-color: var(--btn-primary-hover);\n}\n\nbutton.remove {\n  background-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n  background-color: var(--btn-remove-hover);\n}\n\n#components-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n}\n\n.tab {\n  padding: 10px 20px;\n  background-color: var(--highlight-color);\n  cursor: pointer;\n  border-top-left-radius: 5px;\n  border-top-right-radius: 5px;\n  margin-right: 5px;\n  position: relative;\n  outline: none;\n  border: none;\n  color: var(--text-color);\n  transition: background-color 0.3s;\n}\n\n.tab.active {\n  background-color: var(--card-bg);\n  font-weight: bold;\n}\n\n.tab:hover {\n  background-color: var(--card-bg);\n}\n\n.tab:focus-visible {\n  box-shadow: 0 0 0 3px #4d90fe;\n  z-index: 1;\n}\n\n.tab-content {\n  display: none;\n}\n\n.tab-content.active {\n  display: block;\n}\n\n.components-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n}\n\n/* Communication tab styles */\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--text-color);\n}\n\n/* Text contrast utility classes */\n\n/* Class for elements that need to be monitored for contrast */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the BirthdayComponent\nexport class BirthdayComponent extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._colors = [\n    &#39;#FF5252&#39;, &#39;#FF4081&#39;, &#39;#E040FB&#39;, &#39;#7C4DFF&#39;,\n    &#39;#536DFE&#39;, &#39;#448AFF&#39;, &#39;#40C4FF&#39;, &#39;#18FFFF&#39;,\n    &#39;#64FFDA&#39;, &#39;#69F0AE&#39;, &#39;#B2FF59&#39;, &#39;#EEFF41&#39;,\n    &#39;#FFFF00&#39;, &#39;#FFD740&#39;, &#39;#FFAB40&#39;, &#39;#FF6E40&#39;];\n\n    this._color = this._colors[Math.floor(Math.random() * this._colors.length)];\n    this._confetti = [&#39;🎉&#39;, &#39;🎈&#39;, &#39;🎊&#39;, &#39;✨&#39;, &#39;🎁&#39;, &#39;🎂&#39;, &#39;🍰&#39;];\n  }\n\n  connectedCallback() {\n    console.log(`🎂 Birthday Component #${this._id}: I&#39;m alive! Time to party!`);\n\n    // Create and attach Shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create the component&#39;s HTML\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          width: 280px;\n          transition: all 0.3s ease;\n        }\n        \n        .birthday-card {\n          background-color: ${this._color};\n          border-radius: 10px;\n          box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n          padding: 20px;\n          color: white;\n          text-align: center;\n          position: relative;\n          overflow: hidden;\n          transition: transform 0.3s ease;\n        }\n        \n        .birthday-card:hover {\n          transform: translateY(-5px) scale(1.02);\n          box-shadow: 0 6px 12px rgba(0,0,0,0.15);\n        }\n        \n        h2 {\n          margin-top: 0;\n          font-size: 1.5em;\n        }\n        \n        .confetti {\n          position: absolute;\n          font-size: 1.5em;\n          animation: fall 3s linear infinite;\n          opacity: 0;\n        }\n        \n        .button {\n          background-color: rgba(255, 255, 255, 0.25);\n          border: none;\n          color: white;\n          padding: 8px 16px;\n          border-radius: 20px;\n          margin-top: 10px;\n          cursor: pointer;\n          font-weight: bold;\n          transition: background-color 0.3s;\n        }\n        \n        .button:hover {\n          background-color: rgba(255, 255, 255, 0.4);\n        }\n        \n        .remove-btn {\n          position: absolute;\n          top: 10px;\n          right: 10px;\n          background: rgba(255, 255, 255, 0.3);\n          border: none;\n          color: white;\n          width: 24px;\n          height: 24px;\n          border-radius: 50%;\n          font-weight: bold;\n          cursor: pointer;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n        \n        @keyframes fall {\n          0% {\n            transform: translateY(-20px);\n            opacity: 1;\n          }\n          80% {\n            opacity: 1;\n          }\n          100% {\n            transform: translateY(300px);\n            opacity: 0;\n          }\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;birthday-card&quot;&gt;\n        &lt;button class=&quot;remove-btn&quot;&gt;×&lt;/button&gt;\n        &lt;h2&gt;Hello World!&lt;/h2&gt;\n        &lt;p&gt;I was born at ${new Date().toLocaleTimeString()}&lt;/p&gt;\n        &lt;p&gt;Component ID: #${this._id}&lt;/p&gt;\n        &lt;button class=&quot;button&quot;&gt;Celebrate!&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;.button&#39;).addEventListener(&#39;click&#39;, () =&gt; this.celebrate());\n    this.shadowRoot.querySelector(&#39;.remove-btn&#39;).addEventListener(&#39;click&#39;, () =&gt; this.remove());\n\n    // Start our periodic activity\n    this._activityInterval = setInterval(() =&gt; {\n      console.log(`Birthday Component #${this._id}: Still celebrating at ${new Date().toLocaleTimeString()}`);\n    }, 5000);\n  }\n\n  celebrate() {\n    console.log(`Birthday Component #${this._id}: Woohoo! Extra celebration!`);\n\n    // Create confetti\n    for (let i = 0; i &lt; 15; i++) {\n      this.addConfetti();\n    }\n\n    // Change background color\n    const newColor = this._colors[Math.floor(Math.random() * this._colors.length)];\n    this.shadowRoot.querySelector(&#39;.birthday-card&#39;).style.backgroundColor = newColor;\n  }\n\n  addConfetti() {\n    const confetti = document.createElement(&#39;span&#39;);\n    confetti.classList.add(&#39;confetti&#39;);\n    confetti.textContent = this._confetti[Math.floor(Math.random() * this._confetti.length)];\n    confetti.style.left = `${Math.random() * 100}%`;\n    confetti.style.animationDuration = `${2 + Math.random() * 3}s`;\n    confetti.style.animationDelay = `${Math.random() * 0.5}s`;\n\n    this.shadowRoot.querySelector(&#39;.birthday-card&#39;).appendChild(confetti);\n\n    // Remove the confetti after animation completes\n    setTimeout(() =&gt; {\n      if (confetti.parentNode) {\n        confetti.parentNode.removeChild(confetti);\n      }\n    }, 5000);\n  }\n\n  disconnectedCallback() {\n    console.log(`💫 Birthday Component #${this._id}: It&#39;s been a pleasure serving you! Cleaning up...`);\n\n    // Clear our interval to prevent memory leaks\n    if (this._activityInterval) {\n      clearInterval(this._activityInterval);\n    }\n\n    // Remove any event listeners if needed\n    const button = this.shadowRoot?.querySelector(&#39;.button&#39;);\n    if (button) {\n      button.removeEventListener(&#39;click&#39;, this.celebrate);\n    }\n\n    const removeBtn = this.shadowRoot?.querySelector(&#39;.remove-btn&#39;);\n    if (removeBtn) {\n      removeBtn.removeEventListener(&#39;click&#39;, () =&gt; this.remove());\n    }\n  }}\n\n\n// Register the component\ncustomElements.define(&#39;birthday-card&#39;, BirthdayComponent);\n\n// UI Event Handlers for Birthday Components\ndocument.getElementById(&#39;add-birthday&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;birthday-container&#39;);\n  const component = document.createElement(&#39;birthday-card&#39;);\n  container.appendChild(component);\n});\n\ndocument.getElementById(&#39;remove-all-birthday&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;birthday-container&#39;);\n  while (container.firstChild) {\n    container.removeChild(container.firstChild);\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<h3 id=\"2-disconnectedcallback-the-farewell-tour\">2. <code>disconnectedCallback()</code>: The Farewell Tour<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#2-disconnectedcallback-the-farewell-tour\" aria-label=\"Link to this section\">#</a></h3>\n<p>All good things come to an end. When your component is removed from the DOM, use this opportunity for a proper cleanup:</p>\n<pre><code class=\"language-javascript\">class CleanupComponent extends HTMLElement {\n  connectedCallback() {\n    this.intervalId = setInterval(() =&gt; {\n      console.log('Still here...');\n    }, 1000);\n  }\n\n  disconnectedCallback() {\n    console.log('💫 It\\'s been a pleasure serving you! Cleaning up...');\n    clearInterval(this.intervalId);\n    // Remove event listeners, close connections, etc.\n  }\n}\n\ncustomElements.define('cleanup-demo', CleanupComponent);\n</code></pre>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Disconnected Callback Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div aria-labelledby=&quot;tab-cleanup&quot;&gt;\n\t&lt;div class=&quot;controls&quot;&gt;\n\t\t&lt;button id=&quot;add-cleanup&quot; class=&quot;cleanup&quot;&gt;Add Farewell Tour Component&lt;/button&gt;\n\t\t&lt;button id=&quot;remove-all-cleanup&quot; class=&quot;remove&quot;&gt;Remove All Farewell Components&lt;/button&gt;\n\t&lt;/div&gt;\n\n\t&lt;div id=&quot;cleanup-container&quot; class=&quot;components-container&quot;&gt;&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styles */\n:root {\n\t/* Light mode variables */\n\t--bg-color: #f5f5f5;\n\t--text-color: #333;\n\t--card-bg: #fff;\n\t--shadow-color: rgba(0, 0, 0, 0.1);\n\t--border-color: #eee;\n\t--highlight-color: #f0f0f0;\n\n\t/* Button colors */\n\t--btn-primary: #4caf50;\n\t--btn-primary-hover: #45a049;\n\t--btn-cleanup: #2196f3;\n\t--btn-cleanup-hover: #0b7dda;\n\t--btn-mood: #9c27b0;\n\t--btn-mood-hover: #7b1fa2;\n\t--btn-remove: #f44336;\n\t--btn-remove-hover: #d32f2f;\n\t--btn-adopted: #8bc34a;\n\t--btn-adopted-hover: #7cb342;\n\n\t/* Console colors */\n\t--console-bg: #333;\n\t--console-text: #fff;\n\t--console-birthday: #ff9800;\n\t--console-cleanup: #2196f3;\n\t--console-mood: #e91e63;\n\t--console-adopted: #8bc34a;\n\t--console-communication: #4caf50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #121212;\n\t\t--text-color: #e0e0e0;\n\t\t--card-bg: #1e1e1e;\n\t\t--shadow-color: rgba(0, 0, 0, 0.3);\n\t\t--border-color: #333;\n\t\t--highlight-color: #2c2c2c;\n\n\t\t/* Adjust button colors for better contrast in dark mode */\n\t\t--btn-primary: #5cdf60;\n\t\t--btn-primary-hover: #4cd050;\n\t\t--btn-cleanup: #42a6ff;\n\t\t--btn-cleanup-hover: #3b95e5;\n\t\t--btn-mood: #bb4fd0;\n\t\t--btn-mood-hover: #a53fb8;\n\t\t--btn-remove: #ff6b60;\n\t\t--btn-remove-hover: #e55c52;\n\t\t--btn-adopted: #9bd45a;\n\t\t--btn-adopted-hover: #8bc34a;\n\n\t\t/* Console colors (keep high contrast) */\n\t\t--console-bg: #1a1a1a;\n\t\t--console-text: #f0f0f0;\n\t}\n}\n\n/* High contrast mode styles for accessibility testing */\n\n/* Debug controls styling */\n\nbody {\n\tfont-family: &quot;Segoe UI&quot;, Tahoma, Geneva, Verdana, sans-serif;\n\tdisplay: flex;\n\tjustify-content: center;\n\talign-items: center;\n\tmin-height: 100vh;\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tmargin: 0;\n\tpadding: 20px;\n\tline-height: 1.5;\n}\n\n.controls {\n\tmargin-bottom: 20px;\n\tpadding: 15px;\n\tbackground-color: var(--card-bg);\n\tborder-radius: 8px;\n\tbox-shadow: 0 2px 4px var(--shadow-color);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n}\n\nbutton {\n\tbackground-color: var(--btn-primary);\n\tcolor: white;\n\tborder: none;\n\tpadding: 10px 15px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\tfont-size: 16px;\n\ttransition: background-color 0.3s, box-shadow 0.3s;\n\tposition: relative;\n}\n\nbutton:focus-visible {\n\toutline: 3px solid #4d90fe;\n\toutline-offset: 2px;\n}\n\nbutton:hover {\n\tbackground-color: var(--btn-primary-hover);\n}\n\nbutton.cleanup {\n\tbackground-color: var(--btn-cleanup);\n}\n\nbutton.cleanup:hover {\n\tbackground-color: var(--btn-cleanup-hover);\n}\n\nbutton.remove {\n\tbackground-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n\tbackground-color: var(--btn-remove-hover);\n}\n\n#components-container {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 20px;\n}\n\n.tab {\n\tpadding: 10px 20px;\n\tbackground-color: var(--highlight-color);\n\tcursor: pointer;\n\tborder-top-left-radius: 5px;\n\tborder-top-right-radius: 5px;\n\tmargin-right: 5px;\n\tposition: relative;\n\toutline: none;\n\tborder: none;\n\tcolor: var(--text-color);\n\ttransition: background-color 0.3s;\n}\n\n.tab:hover {\n\tbackground-color: var(--card-bg);\n}\n\n.tab:focus-visible {\n\tbox-shadow: 0 0 0 3px #4d90fe;\n\tz-index: 1;\n}\n\n.components-container {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 20px;\n}\n\n/* Communication tab styles */\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n\tcolor: var(--text-color);\n}\n\n/* Text contrast utility classes */\n\n/* Class for elements that need to be monitored for contrast */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the CleanupComponent (Farewell Tour)\nexport class CleanupComponent extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._countdown = 10; // 10 second countdown\n    this._colors = [&quot;#3F51B5&quot;, &quot;#2196F3&quot;, &quot;#03A9F4&quot;, &quot;#00BCD4&quot;, &quot;#009688&quot;];\n    this._color = this._colors[Math.floor(Math.random() * this._colors.length)];\n  }\n\n  connectedCallback() {\n    console.log(`🔄 Cleanup Component #${this._id}: Starting my farewell tour!`);\n\n    // Create and attach Shadow DOM\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Create the component&#39;s HTML\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          width: 280px;\n          transition: all 0.3s ease;\n        }\n        \n        .cleanup-card {\n          background-color: ${this._color};\n          border-radius: 10px;\n          box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n          padding: 20px;\n          color: white;\n          text-align: center;\n          position: relative;\n        }\n        \n        h2 {\n          margin-top: 0;\n          font-size: 1.5em;\n        }\n        \n        .timer {\n          font-size: 2em;\n          font-weight: bold;\n          margin: 10px 0;\n        }\n        \n        .progress-bar {\n          height: 10px;\n          background-color: rgba(255, 255, 255, 0.3);\n          border-radius: 5px;\n          margin: 15px 0;\n          overflow: hidden;\n        }\n        \n        .progress {\n          height: 100%;\n          background-color: white;\n          width: 100%;\n          transition: width 1s linear;\n        }\n        \n        .button {\n          background-color: rgba(255, 255, 255, 0.25);\n          border: none;\n          color: white;\n          padding: 8px 16px;\n          border-radius: 20px;\n          margin-top: 10px;\n          cursor: pointer;\n          font-weight: bold;\n          transition: background-color 0.3s;\n        }\n        \n        .button:hover {\n          background-color: rgba(255, 255, 255, 0.4);\n        }\n        \n        .remove-btn {\n          position: absolute;\n          top: 10px;\n          right: 10px;\n          background: rgba(255, 255, 255, 0.3);\n          border: none;\n          color: white;\n          width: 24px;\n          height: 24px;\n          border-radius: 50%;\n          font-weight: bold;\n          cursor: pointer;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n        \n        @keyframes pulse {\n          0% { transform: scale(1); }\n          50% { transform: scale(1.05); }\n          100% { transform: scale(1); }\n        }\n        \n        .farewell {\n          animation: pulse 1s infinite;\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;cleanup-card&quot;&gt;\n        &lt;button class=&quot;remove-btn&quot;&gt;×&lt;/button&gt;\n        &lt;h2&gt;Farewell Tour&lt;/h2&gt;\n        &lt;p&gt;Component ID: #${this._id}&lt;/p&gt;\n        &lt;p&gt;I&#39;ll self-destruct in:&lt;/p&gt;\n        &lt;div class=&quot;timer&quot;&gt;${this._countdown}&lt;/div&gt;\n        &lt;div class=&quot;progress-bar&quot;&gt;\n          &lt;div class=&quot;progress&quot;&gt;&lt;/div&gt;\n        &lt;/div&gt;\n        &lt;button class=&quot;button&quot;&gt;Reset Timer&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    // Start our periodic activity - this will be cleaned up in disconnectedCallback\n    this._intervalId = setInterval(() =&gt; {\n      console.log(\n      `Cleanup Component #${this._id}: Still here... ${this._countdown} seconds remaining`);\n\n    }, 1000);\n\n    // Set up countdown\n    this.startCountdown();\n\n    // Add event listeners\n    this.shadowRoot.\n    querySelector(&quot;.button&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.resetTimer());\n    this.shadowRoot.\n    querySelector(&quot;.remove-btn&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.remove());\n  }\n\n  startCountdown() {\n    // Reset the progress bar\n    const progressBar = this.shadowRoot.querySelector(&quot;.progress&quot;);\n    progressBar.style.width = &quot;100%&quot;;\n\n    // Start the countdown timer\n    this._countdownTimer = setInterval(() =&gt; {\n      this._countdown--;\n      this.shadowRoot.querySelector(&quot;.timer&quot;).textContent = this._countdown;\n\n      // Update progress bar\n      const percentage = this._countdown / 10 * 100;\n      progressBar.style.width = `${percentage}%`;\n\n      if (this._countdown &lt;= 3) {\n        this.shadowRoot.querySelector(&quot;.cleanup-card&quot;).classList.add(&quot;farewell&quot;);\n      }\n\n      if (this._countdown &lt;= 0) {\n        // Self-destruct when countdown reaches zero\n        clearInterval(this._countdownTimer);\n        console.log(\n        `👋 Cleanup Component #${this._id}: Time&#39;s up! Initiating self-destruction...`);\n\n        // Remove after a short delay to show 0\n        setTimeout(() =&gt; {\n          this.remove();\n        }, 500);\n      }\n    }, 1000);\n  }\n\n  resetTimer() {\n    console.log(`Cleanup Component #${this._id}: Timer reset to 10 seconds!`);\n\n    // Clear existing timer\n    clearInterval(this._countdownTimer);\n\n    // Reset countdown and UI\n    this._countdown = 10;\n    this.shadowRoot.querySelector(&quot;.timer&quot;).textContent = this._countdown;\n    this.shadowRoot.querySelector(&quot;.cleanup-card&quot;).classList.remove(&quot;farewell&quot;);\n\n    // Start a new countdown\n    this.startCountdown();\n  }\n\n  disconnectedCallback() {\n    console.log(\n    `💫 Cleanup Component #${this._id}: It&#39;s been a pleasure serving you! Cleaning up...`);\n\n\n    // Clear our intervals to prevent memory leaks\n    if (this._intervalId) {\n      clearInterval(this._intervalId);\n    }\n\n    if (this._countdownTimer) {\n      clearInterval(this._countdownTimer);\n    }\n\n    // Remove any event listeners\n    const resetButton = this.shadowRoot?.querySelector(&quot;.button&quot;);\n    if (resetButton) {\n      resetButton.removeEventListener(&quot;click&quot;, () =&gt; this.resetTimer());\n    }\n\n    const removeBtn = this.shadowRoot?.querySelector(&quot;.remove-btn&quot;);\n    if (removeBtn) {\n      removeBtn.removeEventListener(&quot;click&quot;, () =&gt; this.remove());\n    }\n  }}\n\n\n// Register the component\ncustomElements.define(&quot;cleanup-demo&quot;, CleanupComponent);\n// UI Event Handlers for Cleanup Components\ndocument.getElementById(&quot;add-cleanup&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const container = document.getElementById(&quot;cleanup-container&quot;);\n  const component = document.createElement(&quot;cleanup-demo&quot;);\n  container.appendChild(component);\n});\n\ndocument.getElementById(&quot;remove-all-cleanup&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const container = document.getElementById(&quot;cleanup-container&quot;);\n  while (container.firstChild) {\n    container.removeChild(container.firstChild);\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<p><strong>Hands-on challenge:</strong> Create a component with a button that removes itself after a countdown. Watch the disconnectedCallback fire in your console!</p>\n<h3 id=\"3-attributechangedcallback-name-oldvalue-newvalue-the-chameleon-effect\">3. <code>attributeChangedCallback(name, oldValue, newValue)</code>: The Chameleon Effect<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#3-attributechangedcallback-name-oldvalue-newvalue-the-chameleon-effect\" aria-label=\"Link to this section\">#</a></h3>\n<p>Components need to react to changes. This callback is your component's sense organ:</p>\n<pre><code class=\"language-javascript\">class MoodRing extends HTMLElement {\n  static get observedAttributes() {\n    return ['mood'];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === 'mood') {\n      console.log(`🔄 Mood changing from ${oldValue || 'unset'} to ${newValue}!`);\n\n      // Update the UI based on the new mood\n      const moodEmojis = {\n        happy: '😀',\n        sad: '😢',\n        angry: '😠',\n        confused: '🤔'\n      };\n\n      this.innerHTML = `\n        &lt;div class=\"mood-display\"&gt;\n          &lt;span style=\"font-size: 3rem\"&gt;${moodEmojis[newValue] || '😐'}&lt;/span&gt;\n          &lt;p&gt;I'm feeling ${newValue || 'neutral'}!&lt;/p&gt;\n        &lt;/div&gt;\n      `;\n    }\n  }\n}\n\ncustomElements.define('mood-ring', MoodRing);\n</code></pre>\n<p>A word on the <code>observedAttributes</code> list, because it is the part that surprises people. An element can have any number of attributes (<code>class</code>, <code>id</code>, <code>style</code>, <code>data-anything</code>), and most of them are none of your component's business. So the browser only calls <code>attributeChangedCallback</code> for the names you list in <code>static get observedAttributes()</code>. It reads that list once, when you call <code>customElements.define()</code>. Leave <code>'mood'</code> off the list and <code>setAttribute('mood', 'sad')</code> still changes the attribute, but your callback never hears about it.</p>\n<p>Notice <code>oldValue</code> too: it is <code>null</code> the first time, when the attribute goes from absent to present, which is why the example prints <code>oldValue || 'unset'</code>.</p>\n<p><strong>Interactive example:</strong> Click the buttons below to change my mood!</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Attribute Changed Callback Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div&gt;\n        &lt;div class=&quot;controls&quot;&gt;\n          &lt;button id=&quot;add-mood&quot; class=&quot;mood&quot;&gt;Add Mood Ring Component&lt;/button&gt;\n          &lt;button id=&quot;remove-all-mood&quot; class=&quot;remove&quot;&gt;Remove All Mood Components&lt;/button&gt;\n          \n          &lt;div class=&quot;mood-buttons&quot;&gt;\n            &lt;p&gt;Change all moods to:&lt;/p&gt;\n            &lt;button class=&quot;mood-button happy&quot; data-mood=&quot;happy&quot;&gt;Happy&lt;/button&gt;\n            &lt;button class=&quot;mood-button sad&quot; data-mood=&quot;sad&quot;&gt;Sad&lt;/button&gt;\n            &lt;button class=&quot;mood-button angry&quot; data-mood=&quot;angry&quot;&gt;Angry&lt;/button&gt;\n            &lt;button class=&quot;mood-button confused&quot; data-mood=&quot;confused&quot;&gt;Confused&lt;/button&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div id=&quot;mood-container&quot; class=&quot;components-container&quot;&gt;&lt;/div&gt;\n      &lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styles */\n:root {\n  /* Light mode variables */\n  --bg-color: #f5f5f5;\n  --text-color: #333;\n  --card-bg: #fff;\n  --shadow-color: rgba(0,0,0,0.1);\n  --border-color: #eee;\n  --highlight-color: #f0f0f0;\n  \n  /* Button colors */\n  --btn-primary: #4CAF50;\n  --btn-primary-hover: #45a049;\n  --btn-cleanup: #2196F3;\n  --btn-cleanup-hover: #0b7dda;\n  --btn-mood: #9C27B0;\n  --btn-mood-hover: #7B1FA2;\n  --btn-remove: #f44336;\n  --btn-remove-hover: #d32f2f;\n  --btn-adopted: #8bc34a;\n  --btn-adopted-hover: #7cb342;\n  \n  /* Console colors */\n  --console-bg: #333;\n  --console-text: #fff;\n  --console-birthday: #FF9800;\n  --console-cleanup: #2196F3;\n  --console-mood: #E91E63;\n  --console-adopted: #8bc34a;\n  --console-communication: #4CAF50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-color: #121212;\n    --text-color: #e0e0e0;\n    --card-bg: #1e1e1e;\n    --shadow-color: rgba(0,0,0,0.3);\n    --border-color: #333;\n    --highlight-color: #2c2c2c;\n    \n    /* Adjust button colors for better contrast in dark mode */\n    --btn-primary: #5CDF60;\n    --btn-primary-hover: #4cd050;\n    --btn-cleanup: #42a6ff;\n    --btn-cleanup-hover: #3b95e5;\n    --btn-mood: #bb4fd0;\n    --btn-mood-hover: #a53fb8;\n    --btn-remove: #ff6b60;\n    --btn-remove-hover: #e55c52;\n    --btn-adopted: #9bd45a;\n    --btn-adopted-hover: #8bc34a;\n    \n    /* Console colors (keep high contrast) */\n    --console-bg: #1a1a1a;\n    --console-text: #f0f0f0;\n  }\n}\n\n/* High contrast mode styles for accessibility testing */\n\n/* Debug controls styling */\n\nbody {\n  font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.controls {\n  margin-bottom: 20px;\n  padding: 15px;\n  background-color: var(--card-bg);\n  border-radius: 8px;\n  box-shadow: 0 2px 4px var(--shadow-color);\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n}\n\nbutton {\n  background-color: var(--btn-primary);\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.3s, box-shadow 0.3s;\n  position: relative;\n}\n\nbutton:focus-visible {\n  outline: 3px solid #4d90fe;\n  outline-offset: 2px;\n}\n\nbutton:hover {\n  background-color: var(--btn-primary-hover);\n}\n\nbutton.mood {\n  background-color: var(--btn-mood);\n}\n\nbutton.mood:hover {\n  background-color: var(--btn-mood-hover);\n}\n\nbutton.remove {\n  background-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n  background-color: var(--btn-remove-hover);\n}\n\n.mood-buttons {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 5px;\n  margin-top: 10px;\n}\n\n.mood-button {\n  padding: 8px 12px;\n  border-radius: 4px;\n  border: none;\n  cursor: pointer;\n  font-weight: bold;\n  color: white;\n}\n\n.happy { background-color: #FFC107; }\n.sad { background-color: #2196F3; }\n.angry { background-color: #F44336; }\n.confused { background-color: #9C27B0; }\n\n#components-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n}\n\n.components-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n}\n\n/* Communication tab styles */\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--text-color);\n}\n\n/* Text contrast utility classes */\n\n/* Class for elements that need to be monitored for contrast */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the MoodRing Component (Chameleon Effect)\nexport class MoodRing extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._boundEventHandlers = {}; // Store bound event handlers for proper removal\n  }\n\n  // Define which attributes to observe\n  static get observedAttributes() {\n    return [&#39;mood&#39;];\n  }\n\n  connectedCallback() {\n    console.log(`😊 Mood Ring Component #${this._id}: I&#39;m ready to express my feelings!`);\n\n    // Create and attach Shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Set initial mood if not set\n    if (!this.hasAttribute(&#39;mood&#39;)) {\n      this.setAttribute(&#39;mood&#39;, &#39;neutral&#39;);\n    } else {\n      // Force a render if mood was already set\n      this.renderMood(this.getAttribute(&#39;mood&#39;));\n    }\n  }\n\n  // Called when observed attributes change - this is the &quot;Chameleon Effect&quot;\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;mood&#39; &amp;&amp; newValue) {\n      console.log(`🔄 Mood Ring Component #${this._id}: Mood changing from ${oldValue || &#39;unset&#39;} to ${newValue}!`);\n      this.renderMood(newValue);\n    }\n  }\n\n  renderMood(mood) {\n    // Define mood properties\n    const moodEmojis = {\n      happy: &#39;😀&#39;,\n      sad: &#39;😢&#39;,\n      angry: &#39;😠&#39;,\n      confused: &#39;🤔&#39;,\n      neutral: &#39;😐&#39; };\n\n\n    const moodColors = {\n      happy: &#39;#FFC107&#39;,\n      sad: &#39;#2196F3&#39;,\n      angry: &#39;#F44336&#39;,\n      confused: &#39;#9C27B0&#39;,\n      neutral: &#39;#607D8B&#39; };\n\n\n    const moodAnimations = {\n      happy: &#39;bounce 0.5s infinite alternate&#39;,\n      sad: &#39;droop 2s infinite&#39;,\n      angry: &#39;shake 0.5s infinite&#39;,\n      confused: &#39;spin 3s infinite&#39;,\n      neutral: &#39;none&#39; };\n\n\n    const moodDescriptions = {\n      happy: &#39;I feel great today! Everything is awesome!&#39;,\n      sad: &#39;Feeling blue... could use a hug.&#39;,\n      angry: &#39;Grrr! Something\\&#39;s really bothering me!&#39;,\n      confused: &#39;Wait, what? I\\&#39;m not sure what\\&#39;s happening...&#39;,\n      neutral: &#39;Just another day, nothing special.&#39; };\n\n\n    // Ensure we have a valid mood\n    const currentMood = mood in moodEmojis ? mood : &#39;neutral&#39;;\n    const emoji = moodEmojis[currentMood];\n    const color = moodColors[currentMood];\n    const animation = moodAnimations[currentMood];\n    const description = moodDescriptions[currentMood];\n\n    // Create the component&#39;s HTML\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          width: 280px;\n          transition: all 0.3s ease;\n        }\n        \n        .mood-card {\n          background-color: ${color};\n          border-radius: 10px;\n          box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n          padding: 20px;\n          color: white;\n          text-align: center;\n          position: relative;\n        }\n        \n        h2 {\n          margin-top: 0;\n          font-size: 1.5em;\n        }\n        \n        .emoji {\n          font-size: 5em;\n          margin: 15px 0;\n          display: inline-block;\n          animation: ${animation};\n        }\n        \n        .mood-buttons {\n          display: flex;\n          flex-wrap: wrap;\n          justify-content: center;\n          gap: 5px;\n          margin-top: 15px;\n        }\n        \n        .mood-button {\n          background-color: rgba(255, 255, 255, 0.25);\n          border: none;\n          color: white;\n          padding: 8px 12px;\n          border-radius: 20px;\n          cursor: pointer;\n          font-weight: bold;\n          transition: background-color 0.3s;\n        }\n        \n        .mood-button:hover {\n          background-color: rgba(255, 255, 255, 0.4);\n        }\n        \n        .remove-btn {\n          position: absolute;\n          top: 10px;\n          right: 10px;\n          background: rgba(255, 255, 255, 0.3);\n          border: none;\n          color: white;\n          width: 24px;\n          height: 24px;\n          border-radius: 50%;\n          font-weight: bold;\n          cursor: pointer;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n        \n        @keyframes bounce {\n          0% { transform: translateY(0); }\n          100% { transform: translateY(-10px); }\n        }\n        \n        @keyframes droop {\n          0% { transform: rotate(0deg); }\n          50% { transform: rotate(-5deg); }\n          100% { transform: rotate(0deg); }\n        }\n        \n        @keyframes shake {\n          0% { transform: translateX(0); }\n          25% { transform: translateX(-5px); }\n          50% { transform: translateX(0); }\n          75% { transform: translateX(5px); }\n          100% { transform: translateX(0); }\n        }\n        \n        @keyframes spin {\n          0% { transform: rotate(0deg); }\n          100% { transform: rotate(360deg); }\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;mood-card&quot;&gt;\n        &lt;button class=&quot;remove-btn&quot;&gt;×&lt;/button&gt;\n        &lt;h2&gt;Mood Ring #${this._id}&lt;/h2&gt;\n        &lt;div class=&quot;emoji&quot;&gt;${emoji}&lt;/div&gt;\n        &lt;p&gt;I&#39;m feeling &lt;strong&gt;${currentMood}&lt;/strong&gt;!&lt;/p&gt;\n        &lt;p&gt;${description}&lt;/p&gt;\n        &lt;div class=&quot;mood-buttons&quot;&gt;\n          &lt;button class=&quot;mood-button&quot; data-mood=&quot;happy&quot;&gt;Happy&lt;/button&gt;\n          &lt;button class=&quot;mood-button&quot; data-mood=&quot;sad&quot;&gt;Sad&lt;/button&gt;\n          &lt;button class=&quot;mood-button&quot; data-mood=&quot;angry&quot;&gt;Angry&lt;/button&gt;\n          &lt;button class=&quot;mood-button&quot; data-mood=&quot;confused&quot;&gt;Confused&lt;/button&gt;\n          &lt;button class=&quot;mood-button&quot; data-mood=&quot;neutral&quot;&gt;Neutral&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Clean up old event handlers if they exist\n    this._removeEventListeners();\n\n    // Create properly bound event handlers that we can remove later\n    this._boundEventHandlers.remove = () =&gt; this.remove();\n    this._boundEventHandlers.moodChange = e =&gt; {\n      const newMood = e.target.dataset.mood;\n      if (newMood) {\n        this.setAttribute(&#39;mood&#39;, newMood);\n      }\n    };\n\n    // Add event listeners\n    const removeBtn = this.shadowRoot.querySelector(&#39;.remove-btn&#39;);\n    if (removeBtn) {\n      removeBtn.addEventListener(&#39;click&#39;, this._boundEventHandlers.remove);\n    }\n\n    // Add mood button listeners with properly bound handlers\n    const moodButtons = this.shadowRoot.querySelectorAll(&#39;.mood-button&#39;);\n    if (moodButtons) {\n      moodButtons.forEach(button =&gt; {\n        button.addEventListener(&#39;click&#39;, this._boundEventHandlers.moodChange);\n      });\n    }\n  }\n\n  _removeEventListeners() {\n    // Remove the event listeners if element exists and handler is defined\n    const removeBtn = this.shadowRoot?.querySelector(&#39;.remove-btn&#39;);\n    if (removeBtn &amp;&amp; this._boundEventHandlers.remove) {\n      removeBtn.removeEventListener(&#39;click&#39;, this._boundEventHandlers.remove);\n    }\n\n    const moodButtons = this.shadowRoot?.querySelectorAll(&#39;.mood-button&#39;);\n    if (moodButtons &amp;&amp; this._boundEventHandlers.moodChange) {\n      moodButtons.forEach(button =&gt; {\n        button.removeEventListener(&#39;click&#39;, this._boundEventHandlers.moodChange);\n      });\n    }\n  }\n\n  disconnectedCallback() {\n    console.log(`💫 Mood Ring Component #${this._id}: Goodbye! My final mood was ${this.getAttribute(&#39;mood&#39;)}.`);\n\n    // Remove all event listeners\n    this._removeEventListeners();\n  }}\n\n\n// Register the component\ncustomElements.define(&#39;mood-ring&#39;, MoodRing);\n\n// UI Event Handlers for Mood Ring Components\ndocument.getElementById(&#39;add-mood&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;mood-container&#39;);\n  const component = document.createElement(&#39;mood-ring&#39;);\n\n  // Set a random initial mood\n  const moods = [&#39;happy&#39;, &#39;sad&#39;, &#39;angry&#39;, &#39;confused&#39;, &#39;neutral&#39;];\n  const randomMood = moods[Math.floor(Math.random() * moods.length)];\n\n  // First append to DOM, then set attribute to ensure component is connected\n  container.appendChild(component);\n\n  // Use setTimeout to ensure the component is fully initialized\n  setTimeout(() =&gt; {\n    component.setAttribute(&#39;mood&#39;, randomMood);\n  }, 0);\n});\n\ndocument.getElementById(&#39;remove-all-mood&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;mood-container&#39;);\n  while (container.firstChild) {\n    container.removeChild(container.firstChild);\n  }\n});\n\n// Global mood change buttons - Fix event handling\nconst moodButtons = document.querySelectorAll(&#39;.mood-buttons .mood-button&#39;);\nmoodButtons.forEach(button =&gt; {\n  button.addEventListener(&#39;click&#39;, e =&gt; {\n    e.stopPropagation(); // Prevent any parent handlers from firing\n    const newMood = button.dataset.mood;\n    console.log(`Global mood change: Setting all rings to ${newMood}`);\n\n    document.querySelectorAll(&#39;mood-ring&#39;).forEach(ring =&gt; {\n      ring.setAttribute(&#39;mood&#39;, newMood);\n    });\n  });\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<pre><code class=\"language-html\">&lt;mood-ring mood=\"happy\"&gt;&lt;/mood-ring&gt;\n&lt;div&gt;\n  &lt;button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'happy')\"&gt;Happy&lt;/button&gt;\n  &lt;button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'sad')\"&gt;Sad&lt;/button&gt;\n  &lt;button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'angry')\"&gt;Angry&lt;/button&gt;\n  &lt;button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'confused')\"&gt;Confused&lt;/button&gt;\n&lt;/div&gt;\n</code></pre>\n<p><strong>Your turn: wake up the mood ring.</strong> This smaller mood ring is asleep. It starts with <code>mood=\"happy\"</code> and shows nothing, and the Sad and Angry buttons do nothing either. Fix it in two steps, pressing Run after each.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Challenge: wake up the mood ring</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;mood-ring mood=&quot;happy&quot;&gt;&lt;/mood-ring&gt;\n&lt;p&gt;\n  &lt;button data-mood=&quot;sad&quot;&gt;Sad&lt;/button&gt;\n  &lt;button data-mood=&quot;angry&quot;&gt;Angry&lt;/button&gt;\n&lt;/p&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\nmood-ring { display: block; font-size: 1.2rem; min-height: 4rem; }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class MoodRing extends HTMLElement {\n  static get observedAttributes() {\n    return []; // TODO 1: list the attribute this ring should watch\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    const emojis = {\n      happy: &#39;😀&#39;,\n      sad: &#39;😢&#39;\n      // TODO 2: add an &#39;angry&#39; case\n    };\n    this.textContent = `${emojis[newValue] || &#39;😐&#39;} I&#39;m feeling ${newValue}!`;\n  }\n}\n\ncustomElements.define(&#39;mood-ring&#39;, MoodRing);\n\ndocument.querySelectorAll(&#39;button[data-mood]&#39;).forEach((button) =&gt; {\n  button.addEventListener(&#39;click&#39;, () =&gt; {\n    document.querySelector(&#39;mood-ring&#39;).setAttribute(&#39;mood&#39;, button.dataset.mood);\n  });\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<p>Expected result after step 1: the ring says <code>😀 I'm feeling happy!</code> on load, and Sad switches it to <code>😢 I'm feeling sad!</code>. Before step 2, Angry shows the neutral <code>😐</code> face. After step 2 it shows <code>😠</code>.</p>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Step 1 is one word in the array. Step 2 is one more <code>name: 'emoji'</code> pair in the object (do not forget the comma after the line above it).</p>\n</aside>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A component sets <code>static get observedAttributes() { return ['mood']; }</code>. You call <code>ring.setAttribute('size', 'big')</code>. What happens?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>attributeChangedCallback</code> runs with <code>name</code> set to <code>'size'</code></li>\n<li>The attribute changes, but <code>attributeChangedCallback</code> is not called</li>\n<li>The browser throws an error because <code>size</code> is not listed</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The attribute changes, but <code>attributeChangedCallback</code> is not called</p>\n<p>Only attributes named in <code>observedAttributes</code> trigger the callback. Any other attribute still changes normally on the element; your component just is not told.</p>\n</div>\n</div>\n</section>\n<h3 id=\"4-adoptedcallback-the-adoption-papers\">4. <code>adoptedCallback()</code>: The Adoption Papers<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#4-adoptedcallback-the-adoption-papers\" aria-label=\"Link to this section\">#</a></h3>\n<p>This one's a bit like finding yourself in a new family. When your element moves to a new document:</p>\n<pre><code class=\"language-javascript\">class AdoptedPet extends HTMLElement {\n  adoptedCallback() {\n    console.log('🏠 New home! Adjusting to my new document...');\n    // Reset state for the new document context\n  }\n}\n\ncustomElements.define('adopted-pet', AdoptedPet);\n</code></pre>\n<p><strong>Fun fact:</strong> While less commonly used, this callback matters for applications that work with iframes or multiple documents!</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Adopted Callback Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div&gt;\n        &lt;div class=&quot;controls&quot;&gt;\n          &lt;button id=&quot;add-adopted&quot; class=&quot;adopted&quot;&gt;Add Adoption Papers&lt;/button&gt;\n          &lt;button id=&quot;batch-adopt&quot; class=&quot;adopted-action&quot;&gt;Batch Adopt All Pets&lt;/button&gt;\n          &lt;button id=&quot;remove-all-adopted&quot; class=&quot;remove&quot;&gt;Remove All Adoption Papers&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;adoption-sections&quot;&gt;\n          &lt;div class=&quot;adoption-area&quot;&gt;\n            &lt;h3&gt;Shelter&lt;/h3&gt;\n            &lt;div id=&quot;adopted-container&quot; class=&quot;components-container&quot;&gt;&lt;/div&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;adoption-area&quot;&gt;\n            &lt;h3&gt;Forever Homes&lt;/h3&gt;\n            &lt;div id=&quot;forever-homes-container&quot; class=&quot;components-container&quot;&gt;&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styles */\n:root {\n  /* Light mode variables */\n  --bg-color: #f5f5f5;\n  --text-color: #333;\n  --card-bg: #fff;\n  --shadow-color: rgba(0,0,0,0.1);\n  --border-color: #eee;\n  --highlight-color: #f0f0f0;\n  \n  /* Button colors */\n  --btn-primary: #4CAF50;\n  --btn-primary-hover: #45a049;\n  --btn-cleanup: #2196F3;\n  --btn-cleanup-hover: #0b7dda;\n  --btn-mood: #9C27B0;\n  --btn-mood-hover: #7B1FA2;\n  --btn-remove: #f44336;\n  --btn-remove-hover: #d32f2f;\n  --btn-adopted: #8bc34a;\n  --btn-adopted-hover: #7cb342;\n  \n  /* Console colors */\n  --console-bg: #333;\n  --console-text: #fff;\n  --console-birthday: #FF9800;\n  --console-cleanup: #2196F3;\n  --console-mood: #E91E63;\n  --console-adopted: #8bc34a;\n  --console-communication: #4CAF50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-color: #121212;\n    --text-color: #e0e0e0;\n    --card-bg: #1e1e1e;\n    --shadow-color: rgba(0,0,0,0.3);\n    --border-color: #333;\n    --highlight-color: #2c2c2c;\n    \n    /* Adjust button colors for better contrast in dark mode */\n    --btn-primary: #5CDF60;\n    --btn-primary-hover: #4cd050;\n    --btn-cleanup: #42a6ff;\n    --btn-cleanup-hover: #3b95e5;\n    --btn-mood: #bb4fd0;\n    --btn-mood-hover: #a53fb8;\n    --btn-remove: #ff6b60;\n    --btn-remove-hover: #e55c52;\n    --btn-adopted: #9bd45a;\n    --btn-adopted-hover: #8bc34a;\n    \n    /* Console colors (keep high contrast) */\n    --console-bg: #1a1a1a;\n    --console-text: #f0f0f0;\n  }\n}\n\n/* High contrast mode styles for accessibility testing */\n\n/* Debug controls styling */\n\nbody {\n  font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.controls {\n  margin-bottom: 20px;\n  padding: 15px;\n  background-color: var(--card-bg);\n  border-radius: 8px;\n  box-shadow: 0 2px 4px var(--shadow-color);\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n}\n\nbutton {\n  background-color: var(--btn-primary);\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.3s, box-shadow 0.3s;\n  position: relative;\n}\n\nbutton:focus-visible {\n  outline: 3px solid #4d90fe;\n  outline-offset: 2px;\n}\n\nbutton:hover {\n  background-color: var(--btn-primary-hover);\n}\n\nbutton.remove {\n  background-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n  background-color: var(--btn-remove-hover);\n}\n\nbutton.adopted {\n  background-color: var(--btn-adopted);\n}\n\nbutton.adopted:hover {\n  background-color: var(--btn-adopted-hover);\n}\n\nbutton.adopted-action {\n  background-color: var(--btn-primary);\n}\n\nbutton.adopted-action:hover {\n  background-color: var(--btn-primary-hover);\n}\n.sad { background-color: #2196F3; }\n\n#components-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n}\n\n.components-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 20px;\n}\n\n.adoption-sections {\n  display: flex;\n  flex-direction: column;\n  gap: 30px;\n}\n\n.adoption-area {\n  background-color: var(--card-bg);\n  border-radius: 8px;\n  padding: 15px;\n  box-shadow: 0 2px 4px var(--shadow-color);\n}\n\n.adoption-area h3 {\n  margin-top: 0;\n  color: var(--text-color);\n  padding-bottom: 10px;\n  border-bottom: 1px solid var(--border-color);\n  margin-bottom: 15px;\n}\n\n#forever-homes-container {\n  min-height: 100px;\n  background-color: rgba(139, 195, 74, 0.1);\n  border-radius: 8px;\n  padding: 10px;\n}\n\n#adopted-container {\n  min-height: 100px;\n}\n\n/* Communication tab styles */\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--text-color);\n}\n\n/* Text contrast utility classes */\n\n/* Class for elements that need to be monitored for contrast */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define the AdoptedPet Component (demonstrating adoptedCallback)\nexport class AdoptedPet extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._petTypes = [&#39;🐶&#39;, &#39;🐱&#39;, &#39;🐰&#39;, &#39;🐦&#39;, &#39;🐹&#39;, &#39;🦊&#39;, &#39;🐢&#39;, &#39;🦜&#39;, &#39;🐠&#39;];\n    this._petType = this._petTypes[Math.floor(Math.random() * this._petTypes.length)];\n    this._petNames = [&#39;Buddy&#39;, &#39;Luna&#39;, &#39;Charlie&#39;, &#39;Max&#39;, &#39;Bella&#39;, &#39;Daisy&#39;, &#39;Rocky&#39;, &#39;Lucy&#39;, &#39;Bailey&#39;];\n    this._petName = this._petNames[Math.floor(Math.random() * this._petNames.length)];\n    this._homes = 0; // Track how many homes this pet has been in\n    this._homeColors = [&#39;#8bc34a&#39;, &#39;#cddc39&#39;, &#39;#ffeb3b&#39;, &#39;#ffc107&#39;, &#39;#ff9800&#39;, &#39;#ff5722&#39;];\n    this._isBeingAdopted = false; // Flag to prevent double increments\n    this._hasShadowRoot = false; // Flag to track if shadow root has been attached\n  }\n\n  connectedCallback() {\n    console.log(`🏡 Adoption Papers #${this._id}: ${this._petType} ${this._petName} has arrived at the shelter!`);\n\n    // Create and attach Shadow DOM only if it hasn&#39;t been created already\n    if (!this._hasShadowRoot) {\n      this.attachShadow({ mode: &#39;open&#39; });\n      this._hasShadowRoot = true;\n    }\n\n    // Create our unique home ID that we&#39;ll display in the component\n    if (!this._homeId) {\n      this._homeId = document.createElement(&#39;div&#39;).id || Date.now().toString(36);\n    }\n\n    // Render initial state\n    this.render();\n  }\n\n  adoptedCallback() {\n    // Check if this is an actual adoption and not just a DOM reconnection\n    if (!this._isBeingAdopted) {\n      console.log(`🏠 ${this._petType} ${this._petName} (Pet #${this._id}): adoptedCallback triggered but not during adoption process. Ignoring.`);\n      return;\n    }\n\n    this._homes++;\n    console.log(`🏠 ${this._petType} ${this._petName} (Pet #${this._id}): New home! This is home #${this._homes}. Adjusting to my new document...`);\n\n    // Update home ID for the new document\n    this._homeId = document.createElement(&#39;div&#39;).id || Date.now().toString(36);\n\n    // Render with updated home information\n    this.render();\n\n    // Show an adoption celebration animation\n    this.celebrateAdoption();\n\n    // Reset the adoption flag\n    this._isBeingAdopted = false;\n\n    // After adoption, make sure component is moved to forever homes container\n    setTimeout(() =&gt; {\n      const container = document.getElementById(&#39;forever-homes-container&#39;);\n      if (container &amp;&amp; this.parentElement &amp;&amp; this.parentElement.id !== &#39;forever-homes-container&#39;) {\n        container.appendChild(this);\n      }\n    }, 100);\n  }\n\n  celebrateAdoption() {\n    const card = this.shadowRoot.querySelector(&#39;.adoption-card&#39;);\n    if (card) {\n      card.classList.add(&#39;adopted&#39;);\n\n      // Remove the animation class after animation completes\n      setTimeout(() =&gt; {\n        if (card.classList.contains(&#39;adopted&#39;)) {\n          card.classList.remove(&#39;adopted&#39;);\n        }\n      }, 2000);\n    }\n  }\n\n  render() {\n    // Only proceed if we have a shadowRoot\n    if (!this.shadowRoot) return;\n\n    // Get color based on number of homes (first color for first home, etc)\n    const colorIndex = Math.min(this._homes, this._homeColors.length - 1);\n    const cardColor = this._homeColors[colorIndex];\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          width: 280px;\n          transition: all 0.3s ease;\n        }\n        \n        .adoption-card {\n          background-color: ${cardColor};\n          border-radius: 10px;\n          box-shadow: 0 4px 8px rgba(0,0,0,0.1);\n          padding: 20px;\n          color: white;\n          text-align: center;\n          position: relative;\n          overflow: hidden;\n        }\n        \n        .pet-icon {\n          font-size: 5em;\n          margin: 10px 0;\n        }\n        \n        h2 {\n          margin-top: 0;\n          font-size: 1.5em;\n        }\n        \n        .details {\n          margin: 15px 0;\n          font-size: 14px;\n          text-align: left;\n          background: rgba(255, 255, 255, 0.2);\n          padding: 10px;\n          border-radius: 5px;\n        }\n        \n        .adoption-card.adopted {\n          animation: celebrate 2s ease-in-out;\n        }\n        \n        .buttons {\n          display: flex;\n          justify-content: space-between;\n          margin-top: 15px;\n        }\n        \n        .button {\n          background-color: rgba(255, 255, 255, 0.25);\n          border: none;\n          color: white;\n          padding: 8px 16px;\n          border-radius: 20px;\n          cursor: pointer;\n          font-weight: bold;\n          transition: background-color 0.3s;\n          flex: 1;\n          margin: 0 5px;\n        }\n        \n        .button:hover {\n          background-color: rgba(255, 255, 255, 0.4);\n        }\n        \n        .remove-btn {\n          position: absolute;\n          top: 10px;\n          right: 10px;\n          background: rgba(255, 255, 255, 0.3);\n          border: none;\n          color: white;\n          width: 24px;\n          height: 24px;\n          border-radius: 50%;\n          font-weight: bold;\n          cursor: pointer;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n        }\n        \n        @keyframes celebrate {\n          0% { transform: scale(1); }\n          10% { transform: scale(1.1); }\n          20% { transform: scale(1); }\n          30% { transform: scale(1.1); rotate(5deg); }\n          40% { transform: scale(1); rotate(-5deg); }\n          50% { transform: scale(1.05); rotate(0); }\n          60% { transform: scale(1); }\n          100% { transform: scale(1); }\n        }\n        \n        .home-stamp {\n          position: absolute;\n          top: 10px;\n          left: 10px;\n          background: rgba(255, 255, 255, 0.3);\n          font-size: 11px;\n          padding: 3px 8px;\n          border-radius: 10px;\n          font-weight: bold;\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;adoption-card&quot;&gt;\n        &lt;div class=&quot;home-stamp&quot;&gt;Home #${this._homes} (${this._homeId})&lt;/div&gt;\n        &lt;button class=&quot;remove-btn&quot;&gt;×&lt;/button&gt;\n        &lt;div class=&quot;pet-icon&quot;&gt;${this._petType}&lt;/div&gt;\n        &lt;h2&gt;${this._petName}&lt;/h2&gt;\n        &lt;div class=&quot;details&quot;&gt;\n          &lt;p&gt;&lt;strong&gt;Pet ID:&lt;/strong&gt; #${this._id}&lt;/p&gt;\n          &lt;p&gt;&lt;strong&gt;Adoptions:&lt;/strong&gt; ${this._homes} ${this._homes ? &#39;(Previously adopted)&#39; : &#39;(Never adopted)&#39;}&lt;/p&gt;\n          &lt;p&gt;&lt;strong&gt;Current Home:&lt;/strong&gt; ${this._homeId}&lt;/p&gt;\n        &lt;/div&gt;\n        &lt;div class=&quot;buttons&quot;&gt;\n          &lt;button class=&quot;button adopt-btn&quot;&gt;Adopt Me!&lt;/button&gt;\n          &lt;button class=&quot;button return-btn&quot;&gt;Return to Shelter&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;.remove-btn&#39;).addEventListener(&#39;click&#39;, () =&gt; this.remove());\n    this.shadowRoot.querySelector(&#39;.adopt-btn&#39;).addEventListener(&#39;click&#39;, () =&gt; this.adoptPet());\n    this.shadowRoot.querySelector(&#39;.return-btn&#39;).addEventListener(&#39;click&#39;, () =&gt; this.returnToShelter());\n  }\n\n  adoptPet() {\n    console.log(`🏠 ${this._petType} ${this._petName} (Pet #${this._id}): Being adopted to a new home!`);\n\n    // Set the flag to indicate this is an intentional adoption\n    this._isBeingAdopted = true;\n\n    // Create a new temporary document to trigger adoptedCallback\n    const tempDoc = document.implementation.createHTMLDocument(&#39;Adoption Agency&#39;);\n\n    // Move this element to the temporary document to trigger adoptedCallback\n    tempDoc.body.appendChild(this);\n\n    // Move it back to the forever homes container after a short delay\n    setTimeout(() =&gt; {\n      const container = document.getElementById(&#39;forever-homes-container&#39;);\n      if (container) {\n        container.appendChild(this);\n      }\n    }, 100);\n  }\n\n  returnToShelter() {\n    console.log(`😢 ${this._petType} ${this._petName} (Pet #${this._id}): Returned to the shelter.`);\n\n    // Show a sad animation or effect\n    const card = this.shadowRoot.querySelector(&#39;.adoption-card&#39;);\n    card.style.opacity = &#39;0.6&#39;;\n\n    // Reset after a moment\n    setTimeout(() =&gt; {\n      card.style.opacity = &#39;1&#39;;\n\n      // Move back to shelter\n      const container = document.getElementById(&#39;adopted-container&#39;);\n      if (container &amp;&amp; this.parentElement &amp;&amp; this.parentElement.id !== &#39;adopted-container&#39;) {\n        container.appendChild(this);\n      }\n    }, 1000);\n  }\n\n  disconnectedCallback() {\n    console.log(`💫 Adoption Papers #${this._id}: ${this._petType} ${this._petName} has left our system. Good luck!`);\n\n    // Clean up any event listeners or timers if needed\n  }}\n\n\n// Register the component\ncustomElements.define(&#39;adopted-pet&#39;, AdoptedPet);\n\n// UI Event Handlers for Adoption Papers Components\ndocument.getElementById(&#39;add-adopted&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const container = document.getElementById(&#39;adopted-container&#39;);\n  const component = document.createElement(&#39;adopted-pet&#39;);\n  container.appendChild(component);\n});\n\ndocument.getElementById(&#39;remove-all-adopted&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  // Clear both shelter and forever homes containers\n  const shelterContainer = document.getElementById(&#39;adopted-container&#39;);\n  const homesContainer = document.getElementById(&#39;forever-homes-container&#39;);\n\n  while (shelterContainer.firstChild) {\n    shelterContainer.removeChild(shelterContainer.firstChild);\n  }\n\n  while (homesContainer.firstChild) {\n    homesContainer.removeChild(homesContainer.firstChild);\n  }\n});\n\ndocument.getElementById(&#39;batch-adopt&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const pets = document.querySelectorAll(&#39;#adopted-container &gt; adopted-pet&#39;);\n  if (pets.length === 0) {\n    console.log(&#39;No pets available to adopt! Add some first.&#39;);\n    return;\n  }\n\n  console.log(`🏠 Batch adopting ${pets.length} pets to a new home!`);\n\n  // Create a temporary document to move all pets to\n  const tempDoc = document.implementation.createHTMLDocument(&#39;Batch Adoption&#39;);\n\n  // Set the adoption flag on all pets before moving them\n  pets.forEach(pet =&gt; {\n    pet._isBeingAdopted = true;\n  });\n\n  // Move all pets to trigger adoptedCallback for each\n  pets.forEach(pet =&gt; {\n    tempDoc.body.appendChild(pet);\n  });\n\n  // After a short delay, return them to the forever homes container\n  setTimeout(() =&gt; {\n    const container = document.getElementById(&#39;forever-homes-container&#39;);\n    if (container) {\n      while (tempDoc.body.firstChild) {\n        container.appendChild(tempDoc.body.firstChild);\n      }\n    }\n  }, 100);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<h2 id=\"component-communication-let-s-talk\">Component Communication: Let's Talk!<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#component-communication-let-s-talk\" aria-label=\"Link to this section\">#</a></h2>\n<p>Web Components are fantastic at encapsulation, but isolation isn't always desirable. A component that cannot tell anyone anything is a poster on a wall. Here's how to make them communicate effectively. The short version of the rules: data goes <strong>down</strong> through attributes and properties, and news goes <strong>up</strong> through events.</p>\n<h3 id=\"1-parent-child-communication-the-family-chat\">1. Parent-Child Communication: The Family Chat<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#1-parent-child-communication-the-family-chat\" aria-label=\"Link to this section\">#</a></h3>\n<p>Just like in human families, parent and child components can have rich communication:</p>\n<h4 id=\"parents-children-passing-down-wisdom\">Parents → Children: Passing Down Wisdom<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#parents-children-passing-down-wisdom\" aria-label=\"Link to this section\">#</a></h4>\n<p>Parents can pass data to children through attributes or properties. Here is a first draft, with the child watching a <code>message</code> attribute. (Spoiler: it has a bug, and finding it is the point.)</p>\n<pre><code class=\"language-javascript\">class ParentComponent extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div class=\"parent\"&gt;\n        &lt;h2&gt;I'm the parent&lt;/h2&gt;\n        &lt;child-component message=\"Be good!\"&gt;&lt;/child-component&gt;\n        &lt;button id=\"change-message\"&gt;Change Message&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    this.querySelector('#change-message').addEventListener('click', () =&gt; {\n      const child = this.querySelector('child-component');\n      child.setAttribute('message', 'Clean your room!');\n    });\n  }\n}\n\nclass ChildComponent extends HTMLElement {\n  static get observedAttributes() {\n    return ['message'];\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === 'message') {\n      this.querySelector('.message').textContent = newValue;\n    }\n  }\n\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div class=\"child\" style=\"border: 1px solid blue; padding: 10px; margin: 10px 0;\"&gt;\n        &lt;h3&gt;I'm the child&lt;/h3&gt;\n        &lt;p&gt;Parent says: &lt;span class=\"message\"&gt;${this.getAttribute('message') || 'Nothing yet'}&lt;/span&gt;&lt;/p&gt;\n        &lt;button class=\"respond\"&gt;Respond to Parent&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    this.querySelector('.respond').addEventListener('click', () =&gt; {\n      this.dispatchEvent(new CustomEvent('child-response', {\n        bubbles: true,\n        detail: { response: 'Okay, I will!' }\n      }));\n    });\n  }\n}\n\ncustomElements.define('parent-component', ParentComponent);\ncustomElements.define('child-component', ChildComponent);\n</code></pre>\n<p>That looks right, and it fails. Run the first draft below. The red message is the error the browser reports:</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Worked example: the first draft throws</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;pre id=&quot;report&quot;&gt;Waiting...&lt;/pre&gt;\n&lt;parent-component&gt;&lt;/parent-component&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\n#report { color: #b00020; white-space: pre-wrap; }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">const report = document.getElementById(&#39;report&#39;);\nwindow.addEventListener(&#39;error&#39;, (e) =&gt; { report.textContent = &#39;Error: &#39; + e.message; });\n\nclass ChildComponent extends HTMLElement {\n  static get observedAttributes() { return [&#39;message&#39;]; }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    this.querySelector(&#39;.message&#39;).textContent = newValue; // line that throws\n  }\n\n  connectedCallback() {\n    this.innerHTML = `&lt;p&gt;Parent says: &lt;span class=&quot;message&quot;&gt;${this.getAttribute(&#39;message&#39;)}&lt;/span&gt;&lt;/p&gt;`;\n  }\n}\n\nclass ParentComponent extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `&lt;h2&gt;I&#39;m the parent&lt;/h2&gt;&lt;child-component message=&quot;Be good!&quot;&gt;&lt;/child-component&gt;`;\n  }\n}\n\ncustomElements.define(&#39;parent-component&#39;, ParentComponent);\ncustomElements.define(&#39;child-component&#39;, ChildComponent);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<p>The browser reports <code>TypeError: Cannot set properties of null (setting 'textContent')</code>. Here is why. When the child is created with <code>message=\"Be good!\"</code>, the browser calls <code>attributeChangedCallback</code> for that starting value <strong>first</strong>, and only then <code>connectedCallback</code>. At that moment <code>this.innerHTML</code> has not run, so there is no <code>.message</code> element and <code>querySelector</code> returns <code>null</code>. The browser reports the error and carries on, so the page still looks fine (the child's text appears once <code>connectedCallback</code> runs). That is the sneaky part: nothing looks broken, yet every start-up logs an error and any other code in that callback is skipped.</p>\n<p>The fix is to remember that the callbacks can arrive before the component has built anything. Either guard the callback, or make sure the markup exists before it runs. The guard is the smallest change:</p>\n<pre><code class=\"language-javascript\">attributeChangedCallback(name, oldValue, newValue) {\n  if (name !== 'message') return;\n  const target = this.querySelector('.message');\n  if (!target) return; // not built yet: connectedCallback will read the attribute\n  target.textContent = newValue;\n}\n</code></pre>\n<p>Nothing is lost by skipping the early call, because <code>connectedCallback</code> already reads <code>this.getAttribute('message')</code> when it builds the markup. The live demo further down uses the same idea (it checks <code>this.shadowRoot</code> before touching it). Try it: paste the guard into the runnable above and the red message goes away.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>You write <code>&lt;x-y a=\"1\"&gt;</code> in the page, and <code>x-y</code> is a custom element that observes <code>a</code>. In which order does the browser call the element's callbacks?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li><code>connectedCallback</code>, then <code>attributeChangedCallback</code></li>\n<li><code>constructor</code>, then <code>attributeChangedCallback</code>, then <code>connectedCallback</code></li>\n<li><code>attributeChangedCallback</code>, then <code>constructor</code>, then <code>connectedCallback</code></li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. <code>constructor</code>, then <code>attributeChangedCallback</code>, then <code>connectedCallback</code></p>\n<p>The element is constructed first, the attributes it already has are reported next (once per observed attribute), and only then is it connected. That order is why the first draft above reads the page before <code>connectedCallback</code> has built it.</p>\n</div>\n</div>\n</section>\n<h4 id=\"children-parents-speaking-up\">Children → Parents: Speaking Up<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#children-parents-speaking-up\" aria-label=\"Link to this section\">#</a></h4>\n<p>Children talk back to parents using custom events. Here is the complete <code>ParentComponent</code> with the listener added (the child is unchanged apart from the guard):</p>\n<pre><code class=\"language-javascript\">class ParentComponent extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div class=\"parent\"&gt;\n        &lt;h2&gt;I'm the parent&lt;/h2&gt;\n        &lt;child-component message=\"Be good!\"&gt;&lt;/child-component&gt;\n        &lt;button id=\"change-message\"&gt;Change Message&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    this.querySelector('#change-message').addEventListener('click', () =&gt; {\n      const child = this.querySelector('child-component');\n      child.setAttribute('message', 'Clean your room!');\n    });\n\n    this.addEventListener('child-response', (e) =&gt; {\n      alert(`Child says: ${e.detail.response}`);\n    });\n  }\n}\n</code></pre>\n<p>The listener sits on the parent, yet the event is fired by the child. That works because the child created it with <code>bubbles: true</code>, so it travels up through the parent on its way to the document.</p>\n<p><strong>Try it live:</strong> Watch the following demo to see this parent-child communication in action!</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Parent / Child Communication Web Components  Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div&gt;\n\n\t&lt;parent-component&gt;&lt;/parent-component&gt;\n\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* /* Base styles */\n:root {\n  /* Light mode variables */\n  --bg-color: #f5f5f5;\n  --text-color: #333;\n  --card-bg: #fff;\n  --shadow-color: rgba(0,0,0,0.1);\n  --border-color: #eee;\n  --highlight-color: #f0f0f0;\n  \n  /* Button colors */\n  --btn-primary: #4CAF50;\n  --btn-primary-hover: #45a049;\n  --btn-cleanup: #2196F3;\n  --btn-cleanup-hover: #0b7dda;\n  --btn-mood: #9C27B0;\n  --btn-mood-hover: #7B1FA2;\n  --btn-remove: #f44336;\n  --btn-remove-hover: #d32f2f;\n  --btn-adopted: #8bc34a;\n  --btn-adopted-hover: #7cb342;\n  \n  /* Console colors */\n  --console-bg: #333;\n  --console-text: #fff;\n  --console-birthday: #FF9800;\n  --console-cleanup: #2196F3;\n  --console-mood: #E91E63;\n  --console-adopted: #8bc34a;\n  --console-communication: #4CAF50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-color: #121212;\n    --text-color: #e0e0e0;\n    --card-bg: #1e1e1e;\n    --shadow-color: rgba(0,0,0,0.3);\n    --border-color: #333;\n    --highlight-color: #2c2c2c;\n    \n    /* Adjust button colors for better contrast in dark mode */\n    --btn-primary: #5CDF60;\n    --btn-primary-hover: #4cd050;\n    --btn-cleanup: #42a6ff;\n    --btn-cleanup-hover: #3b95e5;\n    --btn-mood: #bb4fd0;\n    --btn-mood-hover: #a53fb8;\n    --btn-remove: #ff6b60;\n    --btn-remove-hover: #e55c52;\n    --btn-adopted: #9bd45a;\n    --btn-adopted-hover: #8bc34a;\n    \n    /* Console colors (keep high contrast) */\n    --console-bg: #1a1a1a;\n    --console-text: #f0f0f0;\n  }\n}\n\n/* High contrast mode styles for accessibility testing */\n.high-contrast {\n  --bg-color: #000;\n  --text-color: #fff;\n  --card-bg: #222;\n  --border-color: #fff;\n  --highlight-color: #444;\n  \n  /* High contrast button colors */\n  --btn-primary: #0080ff;\n  --btn-primary-hover: #00a0ff;\n  --btn-cleanup: #00c0ff;\n  --btn-cleanup-hover: #00e0ff;\n  --btn-mood: #ff00ff;\n  --btn-mood-hover: #ff40ff;\n  --btn-remove: #ff0000;\n  --btn-remove-hover: #ff4040;\n  --btn-adopted: #00ff00;\n  --btn-adopted-hover: #40ff40;\n  \n  /* Increase text contrast */\n  color: #fff;\n  background-color: #000;\n}\n\n.high-contrast button {\n  border: 2px solid white;\n}\n\n.high-contrast h1, \n.high-contrast h2, \n.high-contrast h3 {\n  color: #fff;\n}\n\n.high-contrast input, \n.high-contrast select {\n  background-color: #000;\n  color: #fff;\n  border: 2px solid #fff;\n}\n\n/* Debug controls styling */\n\nbody {\n  font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\nbutton {\n  background-color: var(--btn-primary);\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.3s, box-shadow 0.3s;\n  position: relative;\n}\n\nbutton:focus-visible {\n  outline: 3px solid #4d90fe;\n  outline-offset: 2px;\n}\n\nbutton:hover {\n  background-color: var(--btn-primary-hover);\n}\n\nbutton.remove {\n  background-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n  background-color: var(--btn-remove-hover);\n}\n\n/* Communication tab styles */\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--text-color);\n}\n\n/* Text contrast utility classes */\n.auto-contrast-text {\n  color: var(--auto-text-color, inherit);\n}\n\n/* Class for elements that need to be monitored for contrast */\n.contrast-monitor {\n  position: relative;\n  transition: color 0.3s ease;\n}\n */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">const EventBus = new EventTarget();\n/**\n * Utility functions for color management and accessibility\n */\n\n/**\n * Determines if a color is light or dark\n * @param {string} color - Hex color code\n * @returns {boolean} True if color is light, false if dark\n */\nfunction isLightColor(color) {\n  // Remove the hash at the start if it exists\n  color = color.replace(/^#/, &quot;&quot;);\n\n  // Parse the color\n  let r, g, b;\n  if (color.length === 3) {\n    // Short hex format (e.g. #ABC)\n    r = parseInt(color.charAt(0) + color.charAt(0), 16);\n    g = parseInt(color.charAt(1) + color.charAt(1), 16);\n    b = parseInt(color.charAt(2) + color.charAt(2), 16);\n  } else if (color.length === 6) {\n    // Full hex format (e.g. #AABBCC)\n    r = parseInt(color.substring(0, 2), 16);\n    g = parseInt(color.substring(2, 4), 16);\n    b = parseInt(color.substring(4, 6), 16);\n  } else {\n    // Handle RGB format or other non-hex cases\n    try {\n      if (color.startsWith(&quot;rgb&quot;)) {\n        const rgbValues = color.match(/\\d+/g);\n        if (rgbValues &amp;&amp; rgbValues.length &gt;= 3) {\n          r = parseInt(rgbValues[0]);\n          g = parseInt(rgbValues[1]);\n          b = parseInt(rgbValues[2]);\n        } else {\n          return true; // Default to light if we can&#39;t parse\n        }\n      } else {\n        return true; // Default to light for unknown formats\n      }\n    } catch (e) {\n      console.warn(&quot;Could not parse color:&quot;, color);\n      return true; // Default to light\n    }\n  }\n\n  // Calculate the perceived brightness using the YIQ formula\n  // This gives better results than simply averaging RGB values\n  const yiq = (r * 299 + g * 587 + b * 114) / 1000;\n\n  // YIQ &lt; 128 is considered dark, &gt;= 128 is light\n  return yiq &gt;= 128;\n}\n\n/**\n * Gets the appropriate text color (black or white) for a given background color\n * @param {string} backgroundColor - Background color in hex format\n * @returns {string} - Either &quot;black&quot; or &quot;white&quot;\n */\nfunction getContrastTextColor(backgroundColor) {\n  return isLightColor(backgroundColor) ? &quot;black&quot; : &quot;white&quot;;\n}\n\n/**\n * Applies the appropriate text color to all elements within a container\n * @param {HTMLElement} container - The container element\n * @param {string} backgroundColor - Background color in hex format\n */\nfunction applyContrastText(container, backgroundColor) {\n  const textColor = getContrastTextColor(backgroundColor);\n  container.style.color = textColor;\n\n  // Also handle links and other text elements that might need contrast\n  const links = container.querySelectorAll(&quot;a&quot;);\n  links.forEach(link =&gt; {\n    link.style.color = textColor === &quot;white&quot; ? &quot;#9cceff&quot; : &quot;#0066cc&quot;;\n  });\n}\n\n// Parent-Child Communication: The Family Talk demo\nclass ParentComponent extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._responseCount = 0;\n  }\n\n  connectedCallback() {\n    console.log(`👨‍👩‍👧‍👦 Parent Component #${this._id}: Setting up the family!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Initial render\n    this.render();\n\n    // Listen for child responses\n    this.shadowRoot.addEventListener(\n    &quot;child-response&quot;,\n    this.handleChildResponse.bind(this));\n\n  }\n\n  handleChildResponse(e) {\n    this._responseCount++;\n\n    // Update the response display\n    const responseEl = this.shadowRoot.querySelector(&quot;.child-responses&quot;);\n    if (responseEl) {\n      responseEl.textContent = `Child says: ${e.detail.response}`;\n\n      // Add &quot;times&quot; indicator if multiple responses\n      if (this._responseCount &gt; 1) {\n        responseEl.textContent += ` (${this._responseCount} times)`;\n      }\n\n      // Highlight the new response\n      responseEl.classList.add(&quot;highlight&quot;);\n      setTimeout(() =&gt; {\n        responseEl.classList.remove(&quot;highlight&quot;);\n      }, 1000);\n    }\n\n    console.log(\n    `👨‍👩‍👧‍👦 Parent Component #${this._id}: Received response from child: &quot;${e.detail.response}&quot;`);\n\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          width: 100%;\n          margin: 20px 0;\n        }\n        \n        .parent-card {\n          background-color: var(--card-bg, #f8f9fa);\n          border-radius: 10px;\n          border: 2px solid #4285f4;\n          padding: 15px;\n          font-family: &#39;Segoe UI&#39;, sans-serif;\n          color: var(--text-color, #333);\n        }\n        \n        .parent-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          margin-bottom: 10px;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 10px;\n        }\n        \n        h2 {\n          margin: 0;\n          color: #4285f4;\n        }\n        \n        .children-container {\n          padding: 10px;\n          background-color: var(--highlight-color, #f0f4f8);\n          border-radius: 8px;\n        }\n        \n        .response-panel {\n          margin-top: 15px;\n          padding: 10px;\n          background-color: var(--highlight-color, #e8eaf6);\n          border-radius: 8px;\n        }\n        \n        .child-responses {\n          padding: 8px;\n          color: #5c6bc0;\n          font-weight: bold;\n        }\n        \n        .highlight {\n          animation: highlight 1s ease-in-out;\n        }\n        \n        @keyframes highlight {\n          0% { \n            background-color: rgba(197, 202, 233, 0.5); \n            color: ${getContrastTextColor(&quot;rgba(197, 202, 233, 0.5)&quot;)}; \n          }\n          100% { \n            background-color: transparent; \n          }\n        }\n        \n        button {\n          background-color: #4285f4;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 12px;\n          cursor: pointer;\n          transition: background-color 0.3s;\n        }\n        \n        button:hover {\n          background-color: #3367d6;\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #a1c4fd;\n          outline-offset: 2px;\n        }\n        \n        .message-input {\n          display: flex;\n          gap: 10px;\n          margin-top: 10px;\n          margin-bottom: 10px;\n        }\n        \n        input {\n          flex: 1;\n          padding: 8px;\n          border: 1px solid var(--border-color, #ccc);\n          border-radius: 4px;\n          background-color: var(--card-bg, #fff);\n          color: var(--text-color, #333);\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .parent-card {\n            border-color: #5c9eff;\n          }\n          \n          h2, h3 {\n            color: #5c9eff;\n          }\n          \n          .child-responses {\n            color: #7986cb;\n          }\n          \n          button:focus-visible {\n            outline-color: #83b9ff;\n          }\n        }\n        \n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .parent-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n        \n        :host-context(.high-contrast) h2,\n        :host-context(.high-contrast) h3 {\n          color: white;\n        }\n        \n        :host-context(.high-contrast) input {\n          background-color: black;\n          color: white;\n          border: 2px solid white;\n        }\n        \n        :host-context(.high-contrast) button {\n          background-color: #0080ff;\n          border: 2px solid white;\n        }\n        \n        :host-context(.high-contrast) button:hover {\n          background-color: #00a0ff;\n        }\n        \n        :host-context(.high-contrast) .children-container,\n        :host-context(.high-contrast) .response-panel {\n          background-color: #333;\n        }\n        \n        /* Add contrast text utilities */\n        .auto-contrast-text {\n          transition: color 0.3s ease;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;parent-card contrast-monitor&quot; role=&quot;region&quot; aria-label=&quot;Parent component demonstration&quot;&gt;\n        &lt;div class=&quot;parent-header&quot;&gt;\n          &lt;h2 id=&quot;parent-heading-${\n    this._id\n    }&quot; class=&quot;auto-contrast-text&quot;&gt;Parent Component #${this._id}&lt;/h2&gt;\n          &lt;span aria-hidden=&quot;true&quot;&gt;👨‍👩‍👧‍👦&lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;message-input&quot;&gt;\n          &lt;input type=&quot;text&quot; id=&quot;message-input&quot; \n            aria-label=&quot;Message to send to child&quot; \n            value=&quot;Do your homework!&quot;&gt;\n          &lt;button id=&quot;send-message&quot; aria-label=&quot;Send message to child component&quot;&gt;Send to Child&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;children-container&quot; aria-labelledby=&quot;parent-heading-${\n    this._id\n    }&quot;&gt;\n          &lt;child-component message=&quot;Be good!&quot;&gt;&lt;/child-component&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;response-panel&quot; aria-live=&quot;polite&quot;&gt;\n          &lt;h3 class=&quot;auto-contrast-text&quot;&gt;Child Responses:&lt;/h3&gt;\n          &lt;div class=&quot;child-responses&quot;&gt;Waiting for response...&lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Apply contrast to the component after rendering\n    this._applyInitialContrast();\n\n    // Add event listeners\n    this.shadowRoot.\n    querySelector(&quot;#send-message&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      const message = this.shadowRoot.querySelector(&quot;#message-input&quot;).value;\n      const child = this.shadowRoot.querySelector(&quot;child-component&quot;);\n      child.setAttribute(&quot;message&quot;, message);\n\n      console.log(\n      `👨‍👩‍👧‍👦 Parent Component #${this._id}: Sent message to child: &quot;${message}&quot;`);\n\n    });\n  }\n\n  _applyInitialContrast() {\n    // Wait for styles to be computed\n    setTimeout(() =&gt; {\n      const card = this.shadowRoot.querySelector(&quot;.parent-card&quot;);\n      const computedStyle = getComputedStyle(card);\n      const bgColor = computedStyle.backgroundColor;\n\n      // Determine text color based on background\n      const textColor = getContrastTextColor(bgColor);\n\n      // Apply to auto-contrast elements\n      const textElements = card.querySelectorAll(&quot;.auto-contrast-text&quot;);\n      textElements.forEach(el =&gt; {\n        el.style.color = textColor;\n      });\n    }, 0);\n  }\n\n  disconnectedCallback() {\n    console.log(\n    `👨‍👩‍👧‍👦 Parent Component #${this._id}: Family disbanded. Cleaning up...`);\n\n\n    // Clean up event listeners\n    this.shadowRoot.removeEventListener(\n    &quot;child-response&quot;,\n    this.handleChildResponse);\n\n  }}\n\n\nclass ChildComponent extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n  }\n\n  static get observedAttributes() {\n    return [&quot;message&quot;];\n  }\n\n  connectedCallback() {\n    console.log(`👶 Child Component #${this._id}: I&#39;m here!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Initial render\n    this.render();\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &quot;message&quot; &amp;&amp; this.shadowRoot) {\n      const messageEl = this.shadowRoot.querySelector(&quot;.parent-message&quot;);\n      if (messageEl) {\n        messageEl.textContent = newValue;\n\n        // Add a brief highlight animation\n        messageEl.classList.add(&quot;new-message&quot;);\n        setTimeout(() =&gt; {\n          messageEl.classList.remove(&quot;new-message&quot;);\n        }, 1000);\n      }\n\n      console.log(\n      `👶 Child Component #${this._id}: Received message from parent: &quot;${newValue}&quot;`);\n\n    }\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 10px 0;\n        }\n        \n        .child-card {\n          background-color: var(--card-bg, white);\n          border-radius: 8px;\n          border: 2px solid #fbbc04;\n          padding: 12px;\n          color: var(--text-color, #333);\n        }\n        \n        .child-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 8px;\n          margin-bottom: 8px;\n        }\n        \n        h3 {\n          margin: 0;\n          color: #fbbc04;\n        }\n        \n        .message-container {\n          padding: 8px;\n          background-color: var(--highlight-color, #fff8e1);\n          border-radius: 6px;\n          margin-bottom: 10px;\n        }\n        \n        .parent-message {\n          font-weight: bold;\n          color: #fb8c00;\n        }\n        \n        .new-message {\n          animation: highlight-message 1s ease-in-out;\n        }\n        \n        @keyframes highlight-message {\n          0% { background-color: rgba(255, 224, 178, 0.5); }\n          100% { background-color: transparent; }\n        }\n        \n        .responses {\n          display: flex;\n          flex-wrap: wrap;\n          gap: 8px;\n        }\n        \n        button {\n          background-color: #fbbc04;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 6px 10px;\n          cursor: pointer;\n          transition: background-color 0.3s;\n          font-size: 0.9em;\n        }\n        \n        button:hover {\n          background-color: #f9a825;\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #ffe082;\n          outline-offset: 2px;\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .child-card {\n            border-color: #ffd54f;\n          }\n          \n          h3 {\n            color: #ffd54f;\n          }\n          \n          .parent-message {\n            color: #ffb74d;\n          }\n          \n          button:focus-visible {\n            outline-color: #ffecb3;\n          }\n        }\n        \n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .child-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n        \n        :host-context(.high-contrast) h3 {\n          color: white;\n        }\n        \n        :host-context(.high-contrast) .parent-message {\n          color: white;\n        }\n        \n        :host-context(.high-contrast) button {\n          background-color: #0080ff;\n          border: 2px solid white;\n        }\n        \n        :host-context(.high-contrast) button:hover {\n          background-color: #00a0ff;\n        }\n        \n        :host-context(.high-contrast) .message-container {\n          background-color: #333;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;child-card&quot; role=&quot;region&quot; aria-label=&quot;Child component demonstration&quot;&gt;\n        &lt;div class=&quot;child-header&quot;&gt;\n          &lt;h3 id=&quot;child-heading-${this._id}&quot;&gt;Child Component #${this._id}&lt;/h3&gt;\n          &lt;span aria-hidden=&quot;true&quot;&gt;👶&lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;message-container&quot; aria-live=&quot;polite&quot;&gt;\n          &lt;p&gt;Parent says: &lt;span class=&quot;parent-message&quot;&gt;${\n    this.getAttribute(&quot;message&quot;) || &quot;Nothing yet&quot;\n    }&lt;/span&gt;&lt;/p&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;responses&quot;&gt;\n          &lt;button class=&quot;respond-yes&quot; aria-label=&quot;Respond with yes I will&quot;&gt;Respond &quot;Yes, I will!&quot;&lt;/button&gt;\n          &lt;button class=&quot;respond-no&quot; aria-label=&quot;Respond with not now&quot;&gt;Respond &quot;Not now!&quot;&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.\n    querySelector(&quot;.respond-yes&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; {\n      this.dispatchEvent(\n      new CustomEvent(&quot;child-response&quot;, {\n        bubbles: true,\n        composed: true, // this is needed to cross shadow DOM boundaries\n        detail: { response: &quot;Yes, I will!&quot; } }));\n\n\n    });\n\n    this.shadowRoot.querySelector(&quot;.respond-no&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n      this.dispatchEvent(\n      new CustomEvent(&quot;child-response&quot;, {\n        bubbles: true,\n        composed: true,\n        detail: { response: &quot;Not now!&quot; } }));\n\n\n    });\n  }\n\n  disconnectedCallback() {\n    console.log(`👶 Child Component #${this._id}: Goodbye!`);\n  }}\n\n\n// Register the components\ncustomElements.define(&quot;parent-component&quot;, ParentComponent);\ncustomElements.define(&quot;child-component&quot;, ChildComponent);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<h3 id=\"2-sibling-communication-the-playground-talk\">2. Sibling Communication: The Playground Talk<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#2-sibling-communication-the-playground-talk\" aria-label=\"Link to this section\">#</a></h3>\n<p>Sometimes components at the same level need to coordinate:</p>\n<pre><code class=\"language-javascript\">class SiblingContainer extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div class=\"container\"&gt;\n        &lt;h2&gt;Sibling Communication&lt;/h2&gt;\n        &lt;sibling-sender&gt;&lt;/sibling-sender&gt;\n        &lt;sibling-receiver&gt;&lt;/sibling-receiver&gt;\n      &lt;/div&gt;\n    `;\n\n    // The parent mediates communication between siblings\n    this.addEventListener('message-sent', (e) =&gt; {\n      const receiver = this.querySelector('sibling-receiver');\n      receiver.receiveMessage(e.detail.message);\n    });\n  }\n}\n\nclass SiblingSender extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div style=\"border: 1px solid green; padding: 10px; margin: 10px 0;\"&gt;\n        &lt;h3&gt;Sibling A (Sender)&lt;/h3&gt;\n        &lt;input type=\"text\" placeholder=\"Type a message\" value=\"Hello sibling!\"&gt;\n        &lt;button&gt;Send to Sibling B&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    this.querySelector('button').addEventListener('click', () =&gt; {\n      const message = this.querySelector('input').value;\n      this.dispatchEvent(new CustomEvent('message-sent', {\n        bubbles: true,\n        detail: { message }\n      }));\n    });\n  }\n}\n\nclass SiblingReceiver extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div style=\"border: 1px solid purple; padding: 10px; margin: 10px 0;\"&gt;\n        &lt;h3&gt;Sibling B (Receiver)&lt;/h3&gt;\n        &lt;p&gt;Message received: &lt;span class=\"message\"&gt;Nothing yet&lt;/span&gt;&lt;/p&gt;\n      &lt;/div&gt;\n    `;\n  }\n\n  receiveMessage(message) {\n    this.querySelector('.message').textContent = message;\n    // Add animation to show the message arrived\n    const messageElement = this.querySelector('.message');\n    messageElement.style.animation = 'highlight 1s';\n    setTimeout(() =&gt; { messageElement.style.animation = ''; }, 1000);\n  }\n}\n\ncustomElements.define('sibling-container', SiblingContainer);\ncustomElements.define('sibling-sender', SiblingSender);\ncustomElements.define('sibling-receiver', SiblingReceiver);\n</code></pre>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Sibling Communication Web Component Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div&gt;\n\t&lt;sibling-container&gt;&lt;/sibling-container&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styles */\n:root {\n  /* Light mode variables */\n  --bg-color: #f5f5f5;\n  --text-color: #333;\n  --card-bg: #fff;\n  --shadow-color: rgba(0,0,0,0.1);\n  --border-color: #eee;\n  --highlight-color: #f0f0f0;\n  \n  /* Button colors */\n  --btn-primary: #4CAF50;\n  --btn-primary-hover: #45a049;\n  --btn-cleanup: #2196F3;\n  --btn-cleanup-hover: #0b7dda;\n  --btn-mood: #9C27B0;\n  --btn-mood-hover: #7B1FA2;\n  --btn-remove: #f44336;\n  --btn-remove-hover: #d32f2f;\n  --btn-adopted: #8bc34a;\n  --btn-adopted-hover: #7cb342;\n  \n  /* Console colors */\n  --console-bg: #333;\n  --console-text: #fff;\n  --console-birthday: #FF9800;\n  --console-cleanup: #2196F3;\n  --console-mood: #E91E63;\n  --console-adopted: #8bc34a;\n  --console-communication: #4CAF50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-color: #121212;\n    --text-color: #e0e0e0;\n    --card-bg: #1e1e1e;\n    --shadow-color: rgba(0,0,0,0.3);\n    --border-color: #333;\n    --highlight-color: #2c2c2c;\n    \n    /* Adjust button colors for better contrast in dark mode */\n    --btn-primary: #5CDF60;\n    --btn-primary-hover: #4cd050;\n    --btn-cleanup: #42a6ff;\n    --btn-cleanup-hover: #3b95e5;\n    --btn-mood: #bb4fd0;\n    --btn-mood-hover: #a53fb8;\n    --btn-remove: #ff6b60;\n    --btn-remove-hover: #e55c52;\n    --btn-adopted: #9bd45a;\n    --btn-adopted-hover: #8bc34a;\n    \n    /* Console colors (keep high contrast) */\n    --console-bg: #1a1a1a;\n    --console-text: #f0f0f0;\n  }\n}\n\n/* High contrast mode styles for accessibility testing */\n.high-contrast {\n  --bg-color: #000;\n  --text-color: #fff;\n  --card-bg: #222;\n  --border-color: #fff;\n  --highlight-color: #444;\n  \n  /* High contrast button colors */\n  --btn-primary: #0080ff;\n  --btn-primary-hover: #00a0ff;\n  --btn-cleanup: #00c0ff;\n  --btn-cleanup-hover: #00e0ff;\n  --btn-mood: #ff00ff;\n  --btn-mood-hover: #ff40ff;\n  --btn-remove: #ff0000;\n  --btn-remove-hover: #ff4040;\n  --btn-adopted: #00ff00;\n  --btn-adopted-hover: #40ff40;\n  \n  /* Increase text contrast */\n  color: #fff;\n  background-color: #000;\n}\n\n.high-contrast button {\n  border: 2px solid white;\n}\n\n.high-contrast h1, \n.high-contrast h2, \n.high-contrast h3 {\n  color: #fff;\n}\n\n.high-contrast input, \n.high-contrast select {\n  background-color: #000;\n  color: #fff;\n  border: 2px solid #fff;\n}\n\n/* Debug controls styling */\n\nbody {\n  font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.controls {\n  margin-bottom: 20px;\n  padding: 15px;\n  background-color: var(--card-bg);\n  border-radius: 8px;\n  box-shadow: 0 2px 4px var(--shadow-color);\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n}\n\nbutton {\n  background-color: var(--btn-primary);\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.3s, box-shadow 0.3s;\n  position: relative;\n}\n\nbutton:focus-visible {\n  outline: 3px solid #4d90fe;\n  outline-offset: 2px;\n}\n\nbutton:hover {\n  background-color: var(--btn-primary-hover);\n}\n\nbutton.remove {\n  background-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n  background-color: var(--btn-remove-hover);\n}\n\n/* Communication tab styles */\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--text-color);\n}\n\n/* Text contrast utility classes */\n\n/* Class for elements that need to be monitored for contrast */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Sibling Communication: Components that talk through a parent/container\n\nexport class SiblingContainer extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n  }\n\n  connectedCallback() {\n    console.log(`🏠 Sibling Container #${this._id}: Setting up the playground!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initial render\n    this.render();\n\n    // Listen for and facilitate messages between siblings\n    this.shadowRoot.addEventListener(&#39;message-sent&#39;, this.relayMessage.bind(this));\n  }\n\n  relayMessage(e) {\n    const message = e.detail.message;\n    const sender = e.detail.sender;\n\n    // Find all receivers and relay the message\n    const receivers = this.shadowRoot.querySelectorAll(&#39;sibling-receiver&#39;);\n    receivers.forEach(receiver =&gt; {\n      receiver.receiveMessage(message, sender);\n    });\n\n    console.log(`🏠 Sibling Container #${this._id}: Relayed message &quot;${message}&quot; from Sibling ${sender}`);\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          width: 100%;\n          margin: 20px 0;\n        }\n        \n        .container-card {\n          background-color: var(--card-bg, #f8f9fa);\n          border-radius: 10px;\n          border: 2px solid #34a853;\n          padding: 15px;\n          font-family: &#39;Segoe UI&#39;, sans-serif;\n          color: var(--text-color, #333);\n        }\n        \n        .container-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          margin-bottom: 15px;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 10px;\n        }\n        \n        h2 {\n          margin: 0;\n          color: #34a853;\n        }\n        \n        .siblings-layout {\n          display: flex;\n          flex-wrap: wrap;\n          gap: 15px;\n        }\n        \n        @media (min-width: 768px) {\n          .siblings-layout {\n            flex-direction: row;\n          }\n          \n          .sibling-column {\n            flex: 1;\n          }\n        }\n        \n        @media (max-width: 767px) {\n          .siblings-layout {\n            flex-direction: column;\n          }\n        }\n        \n        .sibling-column {\n          min-width: 200px;\n        }\n        \n        button {\n          background-color: #34a853;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 12px;\n          cursor: pointer;\n          transition: background-color 0.3s;\n        }\n        \n        button:hover {\n          background-color: #2e7d32;\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #a5d6a7;\n          outline-offset: 2px;\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .container-card {\n            border-color: #4caf50;\n          }\n          \n          h2 {\n            color: #4caf50;\n          }\n          \n          button:focus-visible {\n            outline-color: #81c784;\n          }\n        }\n\n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .container-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n\n        :host-context(.high-contrast) h2 {\n          color: white;\n        }\n\n        :host-context(.high-contrast) input {\n          background-color: black;\n          color: white;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) button {\n          background-color: #0080ff;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) .messages-panel {\n          background-color: #333;\n        }\n\n        :host-context(.high-contrast) .message {\n          border: 1px solid #666;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;container-card&quot; role=&quot;region&quot; aria-labelledby=&quot;container-heading-${this._id}&quot;&gt;\n        &lt;div class=&quot;container-header&quot;&gt;\n          &lt;h2 id=&quot;container-heading-${this._id}&quot;&gt;Sibling Container #${this._id}&lt;/h2&gt;\n          &lt;button id=&quot;add-receiver&quot; aria-label=&quot;Add new receiver component&quot;&gt;Add Receiver&lt;/button&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;siblings-layout&quot;&gt;\n          &lt;div class=&quot;sibling-column senders&quot; role=&quot;group&quot; aria-label=&quot;Message senders&quot;&gt;\n            &lt;sibling-sender id=&quot;sender-1&quot; name=&quot;A&quot;&gt;&lt;/sibling-sender&gt;\n            &lt;sibling-sender id=&quot;sender-2&quot; name=&quot;B&quot;&gt;&lt;/sibling-sender&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;sibling-column receivers&quot; role=&quot;group&quot; aria-label=&quot;Message receivers&quot;&gt;\n            &lt;sibling-receiver id=&quot;receiver-1&quot; name=&quot;X&quot;&gt;&lt;/sibling-receiver&gt;\n            &lt;sibling-receiver id=&quot;receiver-2&quot; name=&quot;Y&quot;&gt;&lt;/sibling-receiver&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;#add-receiver&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      const receiverColumn = this.shadowRoot.querySelector(&#39;.receivers&#39;);\n      const newReceiver = document.createElement(&#39;sibling-receiver&#39;);\n\n      // Generate a unique name for the new receiver\n      const existingReceivers = this.shadowRoot.querySelectorAll(&#39;sibling-receiver&#39;);\n      const nextLetter = String.fromCharCode(88 + existingReceivers.length); // Start from &#39;X&#39;\n\n      newReceiver.setAttribute(&#39;name&#39;, nextLetter);\n      newReceiver.id = `receiver-${existingReceivers.length + 1}`;\n\n      receiverColumn.appendChild(newReceiver);\n\n      console.log(`🏠 Sibling Container #${this._id}: Added new receiver &quot;${nextLetter}&quot;`);\n    });\n  }\n\n  disconnectedCallback() {\n    console.log(`🏠 Sibling Container #${this._id}: Playground closed. Cleaning up...`);\n\n    // Clean up event listeners\n    this.shadowRoot.removeEventListener(&#39;message-sent&#39;, this.relayMessage);\n  }}\n\n\nexport class SiblingSender extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n  }\n\n  static get observedAttributes() {\n    return [&#39;name&#39;];\n  }\n\n  connectedCallback() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n    console.log(`🗣️ Sender ${name} #${this._id}: Ready to send messages!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initial render\n    this.render();\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;name&#39; &amp;&amp; this.shadowRoot) {\n      this.render();\n    }\n  }\n\n  render() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 10px 0;\n        }\n        \n        .sender-card {\n          background-color: var(--card-bg, white);\n          border-radius: 8px;\n          border: 2px solid #ea4335;\n          padding: 12px;\n          color: var(--text-color, #333);\n        }\n        \n        .sender-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 8px;\n          margin-bottom: 8px;\n        }\n        \n        h3 {\n          margin: 0;\n          color: #ea4335;\n        }\n        \n        .send-controls {\n          display: flex;\n          flex-direction: column;\n          gap: 8px;\n        }\n        \n        input {\n          padding: 8px;\n          border: 1px solid var(--border-color, #ddd);\n          border-radius: 4px;\n          background-color: var(--card-bg, white);\n          color: var(--text-color, #333);\n        }\n        \n        button {\n          background-color: #ea4335;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px;\n          cursor: pointer;\n          transition: background-color 0.3s;\n        }\n        \n        button:hover {\n          background-color: #c62828;\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #ef9a9a;\n          outline-offset: 2px;\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .sender-card {\n            border-color: #f44336;\n          }\n          \n          h3 {\n            color: #f44336;\n          }\n          \n          button:focus-visible {\n            outline-color: #e57373;\n          }\n        }\n\n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .sender-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n\n        :host-context(.high-contrast) h3 {\n          color: white;\n        }\n\n        :host-context(.high-contrast) input {\n          background-color: black;\n          color: white;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) button {\n          background-color: #0080ff;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) .messages-panel {\n          background-color: #333;\n        }\n\n        :host-context(.high-contrast) .message {\n          border: 1px solid #666;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;sender-card&quot; role=&quot;region&quot; aria-labelledby=&quot;sender-heading-${this._id}&quot;&gt;\n        &lt;div class=&quot;sender-header&quot;&gt;\n          &lt;h3 id=&quot;sender-heading-${this._id}&quot;&gt;Sender ${name}&lt;/h3&gt;\n          &lt;span aria-hidden=&quot;true&quot;&gt;📣&lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;send-controls&quot;&gt;\n          &lt;input type=&quot;text&quot; class=&quot;message-input&quot; \n            aria-label=&quot;Message to send to receivers&quot; \n            placeholder=&quot;Type a message&quot; \n            value=&quot;Hello from Sender ${name}!&quot;&gt;\n          &lt;button class=&quot;send-button&quot; aria-label=&quot;Send message to all receivers&quot;&gt;Send to All Receivers&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;.send-button&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      const message = this.shadowRoot.querySelector(&#39;.message-input&#39;).value;\n      const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n\n      this.dispatchEvent(new CustomEvent(&#39;message-sent&#39;, {\n        bubbles: true,\n        composed: true,\n        detail: {\n          message,\n          sender: name } }));\n\n\n\n      console.log(`🗣️ Sender ${name} #${this._id}: Sent message &quot;${message}&quot;`);\n    });\n  }\n\n  disconnectedCallback() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n    console.log(`🗣️ Sender ${name} #${this._id}: Signing off!`);\n  }}\n\n\nexport class SiblingReceiver extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._messages = [];\n  }\n\n  static get observedAttributes() {\n    return [&#39;name&#39;];\n  }\n\n  connectedCallback() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n    console.log(`👂 Receiver ${name} #${this._id}: Listening for messages!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initial render\n    this.render();\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === &#39;name&#39; &amp;&amp; this.shadowRoot) {\n      this.render();\n    }\n  }\n\n  receiveMessage(message, sender) {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n\n    // Add message to history\n    this._messages.unshift({\n      text: message,\n      sender,\n      time: new Date().toLocaleTimeString() });\n\n\n    // Keep only the 5 most recent messages\n    if (this._messages.length &gt; 5) {\n      this._messages.pop();\n    }\n\n    // Update the message display\n    this.updateMessages();\n\n    console.log(`👂 Receiver ${name} #${this._id}: Received message &quot;${message}&quot; from Sender ${sender}`);\n  }\n\n  updateMessages() {\n    const messagesList = this.shadowRoot.querySelector(&#39;.messages-list&#39;);\n    if (!messagesList) return;\n\n    messagesList.innerHTML = this._messages.length ?\n    this._messages.map(msg =&gt; `\n          &lt;div class=&quot;message&quot;&gt;\n            &lt;div class=&quot;message-header&quot;&gt;\n              &lt;span class=&quot;sender&quot;&gt;From ${msg.sender}&lt;/span&gt;\n              &lt;span class=&quot;time&quot;&gt;${msg.time}&lt;/span&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;message-body&quot;&gt;${msg.text}&lt;/div&gt;\n          &lt;/div&gt;\n        `).join(&#39;&#39;) :\n    &#39;&lt;div class=&quot;no-messages&quot;&gt;No messages yet&lt;/div&gt;&#39;;\n\n    // Animate the newest message\n    const firstMessage = messagesList.querySelector(&#39;.message&#39;);\n    if (firstMessage) {\n      firstMessage.classList.add(&#39;new&#39;);\n      setTimeout(() =&gt; {\n        firstMessage.classList.remove(&#39;new&#39;);\n      }, 1000);\n    }\n  }\n\n  render() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 10px 0;\n        }\n        \n        .receiver-card {\n          background-color: var(--card-bg, white);\n          border-radius: 8px;\n          border: 2px solid #9c27b0;\n          padding: 12px;\n          color: var(--text-color, #333);\n        }\n        \n        .receiver-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 8px;\n          margin-bottom: 8px;\n        }\n        \n        h3 {\n          margin: 0;\n          color: #9c27b0;\n        }\n        \n        .messages-panel {\n          background-color: var(--highlight-color, #f3e5f5);\n          border-radius: 6px;\n          padding: 8px;\n          max-height: 200px;\n          overflow-y: auto;\n        }\n        \n        .messages-list {\n          display: flex;\n          flex-direction: column;\n          gap: 8px;\n        }\n        \n        .message {\n          background-color: var(--card-bg, white);\n          border-radius: 6px;\n          padding: 8px;\n          box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n        }\n        \n        .message.new {\n          animation: highlight-message 1s ease-in-out;\n        }\n        \n        @keyframes highlight-message {\n          0% { background-color: rgba(225, 190, 231, 0.5); }\n          100% { background-color: var(--card-bg, white); }\n        }\n        \n        .message-header {\n          display: flex;\n          justify-content: space-between;\n          font-size: 0.8em;\n          color: var(--text-color, #666);\n          margin-bottom: 4px;\n          opacity: 0.8;\n        }\n        \n        .message-body {\n          font-weight: bold;\n        }\n        \n        .no-messages {\n          color: var(--text-color, #999);\n          opacity: 0.7;\n          font-style: italic;\n          text-align: center;\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .receiver-card {\n            border-color: #ba68c8;\n          }\n          \n          h3 {\n            color: #ba68c8;\n          }\n        }\n\n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .receiver-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n\n        :host-context(.high-contrast) h3 {\n          color: white;\n        }\n\n        :host-context(.high-contrast) input {\n          background-color: black;\n          color: white;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) button {\n          background-color: #0080ff;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) .messages-panel {\n          background-color: #333;\n        }\n\n        :host-context(.high-contrast) .message {\n          border: 1px solid #666;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;receiver-card&quot; role=&quot;region&quot; aria-labelledby=&quot;receiver-heading-${this._id}&quot;&gt;\n        &lt;div class=&quot;receiver-header&quot;&gt;\n          &lt;h3 id=&quot;receiver-heading-${this._id}&quot;&gt;Receiver ${name}&lt;/h3&gt;\n          &lt;span aria-hidden=&quot;true&quot;&gt;👂&lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;messages-panel&quot;&gt;\n          &lt;div class=&quot;messages-list&quot; role=&quot;log&quot; aria-live=&quot;polite&quot;&gt;\n            &lt;div class=&quot;no-messages&quot;&gt;No messages yet&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Update with any existing messages\n    this.updateMessages();\n  }\n\n  disconnectedCallback() {\n    const name = this.getAttribute(&#39;name&#39;) || &#39;?&#39;;\n    console.log(`👂 Receiver ${name} #${this._id}: Stopping listening!`);\n  }}\n\n\n// Register the components\ncustomElements.define(&#39;sibling-container&#39;, SiblingContainer);\ncustomElements.define(&#39;sibling-sender&#39;, SiblingSender);\ncustomElements.define(&#39;sibling-receiver&#39;, SiblingReceiver);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<h3 id=\"3-global-event-bus-the-town-square\">3. Global Event Bus: The Town Square<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#3-global-event-bus-the-town-square\" aria-label=\"Link to this section\">#</a></h3>\n<p>For components that are far apart in the DOM tree, a global event bus works like a town square:</p>\n<pre><code class=\"language-javascript\">// EventBus.js - The town square where everyone meets\nconst EventBus = new EventTarget();\n\n// Export it so components can import and use it\nexport { EventBus };\n</code></pre>\n<p>Using the event bus in components:</p>\n<pre><code class=\"language-javascript\">import { EventBus } from './EventBus.js';\n\nclass GlobalSender extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div style=\"border: 1px solid red; padding: 10px;\"&gt;\n        &lt;h3&gt;Global Announcer&lt;/h3&gt;\n        &lt;button&gt;Broadcast Announcement&lt;/button&gt;\n      &lt;/div&gt;\n    `;\n\n    this.querySelector('button').addEventListener('click', () =&gt; {\n      EventBus.dispatchEvent(new CustomEvent('global-announcement', {\n        detail: { message: 'Attention everyone! Important announcement!' }\n      }));\n    });\n  }\n}\n\nclass GlobalListener extends HTMLElement {\n  connectedCallback() {\n    this.innerHTML = `\n      &lt;div style=\"border: 1px solid blue; padding: 10px;\"&gt;\n        &lt;h3&gt;Global Listener&lt;/h3&gt;\n        &lt;p&gt;Last announcement: &lt;span class=\"announcement\"&gt;None yet&lt;/span&gt;&lt;/p&gt;\n      &lt;/div&gt;\n    `;\n\n    // Subscribe to global events\n    EventBus.addEventListener('global-announcement', this.handleAnnouncement.bind(this));\n  }\n\n  disconnectedCallback() {\n    // Clean up subscription when element is removed\n    EventBus.removeEventListener('global-announcement', this.handleAnnouncement.bind(this));\n  }\n\n  handleAnnouncement(event) {\n    this.querySelector('.announcement').textContent = event.detail.message;\n  }\n}\n\ncustomElements.define('global-sender', GlobalSender);\ncustomElements.define('global-listener', GlobalListener);\n</code></pre>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Global Communication Between Web Components Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;event-bus-demo&quot;&gt;\n          &lt;global-sender&gt;&lt;/global-sender&gt;\n          &lt;global-listener&gt;&lt;/global-listener&gt;\n          &lt;global-listener&gt;&lt;/global-listener&gt;\n        &lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Base styles */\n:root {\n  /* Light mode variables */\n  --bg-color: #f5f5f5;\n  --text-color: #333;\n  --card-bg: #fff;\n  --shadow-color: rgba(0,0,0,0.1);\n  --border-color: #eee;\n  --highlight-color: #f0f0f0;\n  \n  /* Button colors */\n  --btn-primary: #4CAF50;\n  --btn-primary-hover: #45a049;\n  --btn-cleanup: #2196F3;\n  --btn-cleanup-hover: #0b7dda;\n  --btn-mood: #9C27B0;\n  --btn-mood-hover: #7B1FA2;\n  --btn-remove: #f44336;\n  --btn-remove-hover: #d32f2f;\n  --btn-adopted: #8bc34a;\n  --btn-adopted-hover: #7cb342;\n  \n  /* Console colors */\n  --console-bg: #333;\n  --console-text: #fff;\n  --console-birthday: #FF9800;\n  --console-cleanup: #2196F3;\n  --console-mood: #E91E63;\n  --console-adopted: #8bc34a;\n  --console-communication: #4CAF50;\n}\n\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n  :root {\n    --bg-color: #121212;\n    --text-color: #e0e0e0;\n    --card-bg: #1e1e1e;\n    --shadow-color: rgba(0,0,0,0.3);\n    --border-color: #333;\n    --highlight-color: #2c2c2c;\n    \n    /* Adjust button colors for better contrast in dark mode */\n    --btn-primary: #5CDF60;\n    --btn-primary-hover: #4cd050;\n    --btn-cleanup: #42a6ff;\n    --btn-cleanup-hover: #3b95e5;\n    --btn-mood: #bb4fd0;\n    --btn-mood-hover: #a53fb8;\n    --btn-remove: #ff6b60;\n    --btn-remove-hover: #e55c52;\n    --btn-adopted: #9bd45a;\n    --btn-adopted-hover: #8bc34a;\n    \n    /* Console colors (keep high contrast) */\n    --console-bg: #1a1a1a;\n    --console-text: #f0f0f0;\n  }\n}\n\n/* High contrast mode styles for accessibility testing */\n.high-contrast {\n  --bg-color: #000;\n  --text-color: #fff;\n  --card-bg: #222;\n  --border-color: #fff;\n  --highlight-color: #444;\n  \n  /* High contrast button colors */\n  --btn-primary: #0080ff;\n  --btn-primary-hover: #00a0ff;\n  --btn-cleanup: #00c0ff;\n  --btn-cleanup-hover: #00e0ff;\n  --btn-mood: #ff00ff;\n  --btn-mood-hover: #ff40ff;\n  --btn-remove: #ff0000;\n  --btn-remove-hover: #ff4040;\n  --btn-adopted: #00ff00;\n  --btn-adopted-hover: #40ff40;\n  \n  /* Increase text contrast */\n  color: #fff;\n  background-color: #000;\n}\n\n.high-contrast button {\n  border: 2px solid white;\n}\n\n.high-contrast h1, \n.high-contrast h2, \n.high-contrast h3 {\n  color: #fff;\n}\n\n.high-contrast input, \n.high-contrast select {\n  background-color: #000;\n  color: #fff;\n  border: 2px solid #fff;\n}\n\n/* Debug controls styling */\n\nbody {\n  font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  min-height: 100vh;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  margin: 0;\n  padding: 20px;\n  line-height: 1.5;\n}\n\n.controls {\n  margin-bottom: 20px;\n  padding: 15px;\n  background-color: var(--card-bg);\n  border-radius: 8px;\n  box-shadow: 0 2px 4px var(--shadow-color);\n  display: flex;\n  flex-wrap: wrap;\n  gap: 10px;\n}\n\nbutton {\n  background-color: var(--btn-primary);\n  color: white;\n  border: none;\n  padding: 10px 15px;\n  border-radius: 4px;\n  cursor: pointer;\n  font-size: 16px;\n  transition: background-color 0.3s, box-shadow 0.3s;\n  position: relative;\n}\n\nbutton:focus-visible {\n  outline: 3px solid #4d90fe;\n  outline-offset: 2px;\n}\n\nbutton:hover {\n  background-color: var(--btn-primary-hover);\n}\n\nbutton.remove {\n  background-color: var(--btn-remove);\n}\n\nbutton.remove:hover {\n  background-color: var(--btn-remove-hover);\n}\n\n/* Communication tab styles */\n.event-bus-demo {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 20px;\n  margin-top: 20px;\n}\n\n@media (max-width: 768px) {\n  .event-bus-demo {\n    grid-template-columns: 1fr;\n  }\n}\n\nh1, h2, h3, h4, h5, h6 {\n  color: var(--text-color);\n}\n\n/* Text contrast utility classes */\n\n/* Class for elements that need to be monitored for contrast */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Global Communication: Using an EventBus for components that are far apart\nconst EventBus = new EventTarget();\n\n\nclass GlobalSender extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._messageCount = 0;\n  }\n\n  connectedCallback() {\n    console.log(`🌎 Global Sender #${this._id}: Ready to broadcast globally!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initial render\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 10px 0;\n        }\n        \n        .sender-card {\n          background-color: var(--card-bg, white);\n          border-radius: 8px;\n          border: 2px solid #673ab7;\n          padding: 12px;\n          color: var(--text-color, #333);\n        }\n        \n        .sender-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 8px;\n          margin-bottom: 12px;\n        }\n        \n        h3 {\n          margin: 0;\n          color: #673ab7;\n        }\n        \n        .send-controls {\n          display: flex;\n          flex-direction: column;\n          gap: 10px;\n        }\n        \n        .input-group {\n          display: flex;\n          flex-direction: column;\n          gap: 5px;\n        }\n        \n        label {\n          font-size: 0.9em;\n          color: var(--text-color, #555);\n          opacity: 0.9;\n        }\n        \n        input, select {\n          padding: 8px;\n          border: 1px solid var(--border-color, #ddd);\n          border-radius: 4px;\n          background-color: var(--card-bg, white);\n          color: var(--text-color, #333);\n        }\n        \n        .buttons {\n          display: flex;\n          gap: 8px;\n          margin-top: 5px;\n        }\n        \n        button {\n          flex: 1;\n          background-color: #673ab7;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px;\n          cursor: pointer;\n          transition: background-color 0.3s;\n        }\n        \n        button:hover {\n          background-color: #5e35b1;\n        }\n        \n        button:focus-visible {\n          outline: 3px solid #b39ddb;\n          outline-offset: 2px;\n        }\n        \n        .announcement {\n          background-color: #ff5722;\n        }\n        \n        .announcement:hover {\n          background-color: #e64a19;\n        }\n        \n        .message-count {\n          background-color: var(--highlight-color, #ede7f6);\n          padding: 5px 10px;\n          border-radius: 4px;\n          text-align: center;\n          font-size: 0.9em;\n          margin-top: 10px;\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .sender-card {\n            border-color: #7e57c2;\n          }\n          \n          h3 {\n            color: #7e57c2;\n          }\n          \n          button:focus-visible {\n            outline-color: #9575cd;\n          }\n        }\n\n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .sender-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n\n        :host-context(.high-contrast) h3 {\n          color: white;\n        }\n\n        :host-context(.high-contrast) input,\n        :host-context(.high-contrast) select {\n          background-color: black;\n          color: white;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) button {\n          background-color: #0080ff;\n          border: 2px solid white;\n        }\n\n        :host-context(.high-contrast) .announcement {\n          background-color: #ff0000;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;sender-card&quot; role=&quot;region&quot; aria-labelledby=&quot;sender-heading-${this._id}&quot;&gt;\n        &lt;div class=&quot;sender-header&quot;&gt;\n          &lt;h3 id=&quot;sender-heading-${this._id}&quot;&gt;Global Sender #${this._id}&lt;/h3&gt;\n          &lt;span aria-hidden=&quot;true&quot;&gt;🌎&lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;send-controls&quot;&gt;\n          &lt;div class=&quot;input-group&quot;&gt;\n            &lt;label for=&quot;message-type-${this._id}&quot;&gt;Message Type:&lt;/label&gt;\n            &lt;select id=&quot;message-type-${this._id}&quot; aria-label=&quot;Select message type&quot;&gt;\n              &lt;option value=&quot;global-message&quot;&gt;Regular Message&lt;/option&gt;\n              &lt;option value=&quot;global-announcement&quot;&gt;Important Announcement&lt;/option&gt;\n              &lt;option value=&quot;theme-change&quot;&gt;Theme Change&lt;/option&gt;\n            &lt;/select&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;input-group&quot;&gt;\n            &lt;label for=&quot;message-input-${this._id}&quot;&gt;Message:&lt;/label&gt;\n            &lt;input type=&quot;text&quot; id=&quot;message-input-${this._id}&quot; value=&quot;Hello, world!&quot; aria-label=&quot;Message content&quot;&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;buttons&quot;&gt;\n            &lt;button class=&quot;send-button&quot; aria-label=&quot;Send message to listeners&quot;&gt;Send Message&lt;/button&gt;\n            &lt;button class=&quot;announcement&quot; aria-label=&quot;Send urgent announcement to listeners&quot;&gt;Broadcast Announcement&lt;/button&gt;\n          &lt;/div&gt;\n          \n          &lt;div class=&quot;message-count&quot; aria-live=&quot;polite&quot; aria-atomic=&quot;true&quot;&gt;\n            Messages sent: ${this._messageCount}\n          &lt;/div&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Fix the references to the new unique IDs\n    this._messageTypeElem = this.shadowRoot.querySelector(`#message-type-${this._id}`);\n    this._messageInputElem = this.shadowRoot.querySelector(`#message-input-${this._id}`);\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;.send-button&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      const messageType = this._messageTypeElem.value;\n      const message = this._messageInputElem.value;\n\n      EventBus.dispatchEvent(new CustomEvent(messageType, {\n        detail: {\n          message,\n          sender: this._id,\n          timestamp: new Date().toLocaleTimeString() } }));\n\n\n\n      this._messageCount++;\n      this.shadowRoot.querySelector(&#39;.message-count&#39;).textContent = `Messages sent: ${this._messageCount}`;\n\n      console.log(`🌎 Global Sender #${this._id}: Dispatched &quot;${messageType}&quot; with message &quot;${message}&quot;`);\n    });\n\n    this.shadowRoot.querySelector(&#39;.announcement&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      const message = this._messageInputElem.value || &#39;Important announcement!&#39;;\n\n      EventBus.dispatchEvent(new CustomEvent(&#39;global-announcement&#39;, {\n        detail: {\n          message,\n          sender: this._id,\n          timestamp: new Date().toLocaleTimeString(),\n          urgent: true } }));\n\n\n\n      this._messageCount++;\n      this.shadowRoot.querySelector(&#39;.message-count&#39;).textContent = `Messages sent: ${this._messageCount}`;\n\n      console.log(`🌎 Global Sender #${this._id}: Dispatched urgent announcement &quot;${message}&quot;`);\n    });\n  }\n\n  disconnectedCallback() {\n    console.log(`🌎 Global Sender #${this._id}: Signing off!`);\n  }}\n\n\nclass GlobalListener extends HTMLElement {\n  constructor() {\n    super();\n    this._id = Math.floor(Math.random() * 10000);\n    this._messages = [];\n\n    // Bind event handlers\n    this._boundHandlers = {\n      message: this.handleMessage.bind(this),\n      announcement: this.handleAnnouncement.bind(this),\n      theme: this.handleThemeChange.bind(this) };\n\n  }\n\n  connectedCallback() {\n    console.log(`🌍 Global Listener #${this._id}: Listening to global events!`);\n\n    // Create shadow DOM\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Initial render\n    this.render();\n\n    // Subscribe to global events\n    EventBus.addEventListener(&#39;global-message&#39;, this._boundHandlers.message);\n    EventBus.addEventListener(&#39;global-announcement&#39;, this._boundHandlers.announcement);\n    EventBus.addEventListener(&#39;theme-change&#39;, this._boundHandlers.theme);\n  }\n\n  handleMessage(event) {\n    const data = event.detail;\n\n    // Add message to history\n    this._messages.unshift({\n      type: &#39;message&#39;,\n      ...data });\n\n\n    // Keep only the 10 most recent messages\n    if (this._messages.length &gt; 10) {\n      this._messages.pop();\n    }\n\n    // Update the display\n    this.updateMessages();\n\n    console.log(`🌍 Global Listener #${this._id}: Received message &quot;${data.message}&quot; from Sender #${data.sender}`);\n  }\n\n  handleAnnouncement(event) {\n    const data = event.detail;\n\n    // Add announcement to history\n    this._messages.unshift({\n      type: &#39;announcement&#39;,\n      ...data });\n\n\n    // Keep only the 10 most recent messages\n    if (this._messages.length &gt; 10) {\n      this._messages.pop();\n    }\n\n    // Update the display\n    this.updateMessages();\n\n    // Play an alert sound for urgent announcements\n    if (data.urgent) {\n      this.playAlert();\n    }\n\n    console.log(`🌍 Global Listener #${this._id}: Received ANNOUNCEMENT &quot;${data.message}&quot; from Sender #${data.sender}`);\n  }\n\n  handleThemeChange(event) {\n    const data = event.detail;\n\n    // Add theme change to history\n    this._messages.unshift({\n      type: &#39;theme&#39;,\n      ...data });\n\n\n    // Keep only the 10 most recent messages\n    if (this._messages.length &gt; 10) {\n      this._messages.pop();\n    }\n\n    // Update the display\n    this.updateMessages();\n\n    // Change the card theme\n    const card = this.shadowRoot.querySelector(&#39;.listener-card&#39;);\n    if (card) {\n      card.style.backgroundColor = data.message === &#39;dark&#39; ? &#39;#263238&#39; : &#39;#fff&#39;;\n\n      // Also update text color for dark theme\n      if (data.message === &#39;dark&#39;) {\n        card.style.color = &#39;#fff&#39;;\n      } else {\n        card.style.color = &#39;&#39;;\n      }\n    }\n\n    console.log(`🌍 Global Listener #${this._id}: Received theme change to &quot;${data.message}&quot; from Sender #${data.sender}`);\n  }\n\n  playAlert() {\n    // Visual alert\n    const card = this.shadowRoot.querySelector(&#39;.listener-card&#39;);\n    if (card) {\n      card.classList.add(&#39;urgent-alert&#39;);\n      setTimeout(() =&gt; {\n        card.classList.remove(&#39;urgent-alert&#39;);\n      }, 2000);\n    }\n  }\n\n  updateMessages() {\n    const messagesList = this.shadowRoot.querySelector(&#39;.messages-list&#39;);\n    if (!messagesList) return;\n\n    messagesList.innerHTML = this._messages.length ?\n    this._messages.map(msg =&gt; `\n          &lt;div class=&quot;message ${msg.type} ${msg.urgent ? &#39;urgent&#39; : &#39;&#39;}&quot;&gt;\n            &lt;div class=&quot;message-header&quot;&gt;\n              &lt;span class=&quot;type&quot;&gt;${this.getTypeIcon(msg.type)} ${this.getTypeName(msg.type)}&lt;/span&gt;\n              &lt;span class=&quot;time&quot;&gt;${msg.timestamp}&lt;/span&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;message-body&quot;&gt;${msg.message}&lt;/div&gt;\n            &lt;div class=&quot;message-footer&quot;&gt;\n              &lt;span class=&quot;sender&quot;&gt;From: Sender #${msg.sender}&lt;/span&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        `).join(&#39;&#39;) :\n    &#39;&lt;div class=&quot;no-messages&quot;&gt;No messages yet&lt;/div&gt;&#39;;\n\n    // Animate the newest message\n    const firstMessage = messagesList.querySelector(&#39;.message&#39;);\n    if (firstMessage) {\n      firstMessage.classList.add(&#39;new&#39;);\n      setTimeout(() =&gt; {\n        firstMessage.classList.remove(&#39;new&#39;);\n      }, 1000);\n    }\n  }\n\n  getTypeIcon(type) {\n    switch (type) {\n      case &#39;message&#39;:return &#39;💬&#39;;\n      case &#39;announcement&#39;:return &#39;📢&#39;;\n      case &#39;theme&#39;:return &#39;🎨&#39;;\n      default:return &#39;📩&#39;;}\n\n  }\n\n  getTypeName(type) {\n    switch (type) {\n      case &#39;message&#39;:return &#39;Message&#39;;\n      case &#39;announcement&#39;:return &#39;Announcement&#39;;\n      case &#39;theme&#39;:return &#39;Theme Change&#39;;\n      default:return &#39;Unknown&#39;;}\n\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          margin: 10px 0;\n        }\n        \n        .listener-card {\n          background-color: var(--card-bg, white);\n          border-radius: 8px;\n          border: 2px solid #2196f3;\n          padding: 12px;\n          transition: all 0.3s ease;\n          color: var(--text-color, #333);\n        }\n        \n        .listener-header {\n          display: flex;\n          justify-content: space-between;\n          align-items: center;\n          border-bottom: 1px solid var(--border-color, #eee);\n          padding-bottom: 8px;\n          margin-bottom: 12px;\n        }\n        \n        h3 {\n          margin: 0;\n          color: #2196f3;\n        }\n        \n        .messages-panel {\n          background-color: var(--highlight-color, #e3f2fd);\n          border-radius: 6px;\n          padding: 8px;\n          max-height: 250px;\n          overflow-y: auto;\n        }\n        \n        .messages-list {\n          display: flex;\n          flex-direction: column;\n          gap: 8px;\n        }\n        \n        .message {\n          background-color: var(--card-bg, white);\n          border-radius: 6px;\n          padding: 10px;\n          box-shadow: 0 1px 3px rgba(0,0,0,0.1);\n        }\n        \n        .message.announcement {\n          border-left: 4px solid #ff5722;\n        }\n        \n        .message.announcement.urgent {\n          border-left: 4px solid #f44336;\n          background-color: var(--card-bg, #ffebee);\n        }\n        \n        .message.theme {\n          border-left: 4px solid #9c27b0;\n        }\n        \n        .message.new {\n          animation: highlight-message 1s ease-in-out;\n        }\n        \n        @keyframes highlight-message {\n          0% { transform: translateX(-10px); opacity: 0; }\n          100% { transform: translateX(0); opacity: 1; }\n        }\n        \n        .message-header {\n          display: flex;\n          justify-content: space-between;\n          font-size: 0.85em;\n          color: var(--text-color, #666);\n          opacity: 0.8;\n          margin-bottom: 6px;\n        }\n        \n        .message-body {\n          font-weight: bold;\n          margin-bottom: 6px;\n        }\n        \n        .message-footer {\n          font-size: 0.8em;\n          color: var(--text-color, #999);\n          opacity: 0.8;\n          text-align: right;\n          font-style: italic;\n        }\n        \n        .no-messages {\n          color: var(--text-color, #999);\n          opacity: 0.7;\n          font-style: italic;\n          text-align: center;\n          padding: 20px;\n        }\n        \n        .urgent-alert {\n          animation: urgent-pulse 0.5s ease-in-out 4;\n        }\n        \n        @keyframes urgent-pulse {\n          0% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4); }\n          70% { box-shadow: 0 0 0 10px rgba(244, 67, 54, 0); }\n          100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0); }\n        }\n        \n        .controls {\n          display: flex;\n          justify-content: space-between;\n          margin-top: 12px;\n        }\n        \n        .subscribe-toggle {\n          background-color: #2196f3;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 12px;\n          cursor: pointer;\n        }\n        \n        .subscribe-toggle:hover {\n          background-color: #1976d2;\n        }\n        \n        .subscribe-toggle:focus-visible {\n          outline: 3px solid #90caf9;\n          outline-offset: 2px;\n        }\n        \n        .clear-messages {\n          background-color: #f44336;\n          color: white;\n          border: none;\n          border-radius: 4px;\n          padding: 8px 12px;\n          cursor: pointer;\n        }\n        \n        .clear-messages:hover {\n          background-color: #d32f2f;\n        }\n        \n        .clear-messages:focus-visible {\n          outline: 3px solid #ef9a9a;\n          outline-offset: 2px;\n        }\n        \n        @media (prefers-color-scheme: dark) {\n          .listener-card {\n            border-color: #42a5f5;\n          }\n          \n          h3 {\n            color: #42a5f5;\n          }\n          \n          .message.announcement.urgent {\n            background-color: rgba(244, 67, 54, 0.2);\n          }\n          \n          .subscribe-toggle:focus-visible {\n            outline-color: #64b5f6;\n          }\n          \n          .clear-messages:focus-visible {\n            outline-color: #e57373;\n          }\n        }\n\n        /* Support for high contrast mode */\n        :host-context(.high-contrast) .listener-card {\n          border: 3px solid white;\n          background-color: #222;\n          color: white;\n        }\n\n        :host-context(.high-contrast) h3 {\n          color: white;\n        }\n\n        :host-context(.high-contrast) .messages-panel {\n          background-color: #333;\n        }\n\n        :host-context(.high-contrast) .message {\n          border: 1px solid #666;\n        }\n\n        :host-context(.high-contrast) .message.urgent {\n          border-left: 5px solid #ff0000;\n        }\n      &lt;/style&gt;\n      \n      &lt;div class=&quot;listener-card&quot; role=&quot;region&quot; aria-labelledby=&quot;listener-heading-${this._id}&quot;&gt;\n        &lt;div class=&quot;listener-header&quot;&gt;\n          &lt;h3 id=&quot;listener-heading-${this._id}&quot;&gt;Global Listener #${this._id}&lt;/h3&gt;\n          &lt;span aria-hidden=&quot;true&quot;&gt;🌍&lt;/span&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;messages-panel&quot;&gt;\n          &lt;div class=&quot;messages-list&quot; role=&quot;log&quot; aria-live=&quot;polite&quot;&gt;\n            &lt;div class=&quot;no-messages&quot;&gt;No messages yet - waiting for global broadcasts...&lt;/div&gt;\n          &lt;/div&gt;\n        &lt;/div&gt;\n        \n        &lt;div class=&quot;controls&quot;&gt;\n          &lt;button class=&quot;subscribe-toggle&quot; aria-pressed=&quot;true&quot;&gt;Unsubscribe&lt;/button&gt;\n          &lt;button class=&quot;clear-messages&quot; aria-label=&quot;Clear message history&quot;&gt;Clear Messages&lt;/button&gt;\n        &lt;/div&gt;\n      &lt;/div&gt;\n    `;\n\n    // Add event listeners\n    this.shadowRoot.querySelector(&#39;.subscribe-toggle&#39;).addEventListener(&#39;click&#39;, e =&gt; {\n      const button = e.target;\n      const isSubscribed = button.textContent === &#39;Unsubscribe&#39;;\n\n      if (isSubscribed) {\n        // Unsubscribe from events\n        EventBus.removeEventListener(&#39;global-message&#39;, this._boundHandlers.message);\n        EventBus.removeEventListener(&#39;global-announcement&#39;, this._boundHandlers.announcement);\n        EventBus.removeEventListener(&#39;theme-change&#39;, this._boundHandlers.theme);\n\n        button.textContent = &#39;Subscribe&#39;;\n        button.style.backgroundColor = &#39;#4caf50&#39;;\n\n        this.shadowRoot.querySelector(&#39;.messages-list&#39;).innerHTML =\n        &#39;&lt;div class=&quot;no-messages&quot;&gt;Currently unsubscribed from global events&lt;/div&gt;&#39;;\n\n        console.log(`🌍 Global Listener #${this._id}: Unsubscribed from global events`);\n      } else {\n        // Resubscribe to events\n        EventBus.addEventListener(&#39;global-message&#39;, this._boundHandlers.message);\n        EventBus.addEventListener(&#39;global-announcement&#39;, this._boundHandlers.announcement);\n        EventBus.addEventListener(&#39;theme-change&#39;, this._boundHandlers.theme);\n\n        button.textContent = &#39;Unsubscribe&#39;;\n        button.style.backgroundColor = &#39;#2196f3&#39;;\n\n        this.updateMessages();\n\n        console.log(`🌍 Global Listener #${this._id}: Subscribed to global events`);\n      }\n    });\n\n    this.shadowRoot.querySelector(&#39;.clear-messages&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n      this._messages = [];\n      this.shadowRoot.querySelector(&#39;.messages-list&#39;).innerHTML =\n      &#39;&lt;div class=&quot;no-messages&quot;&gt;No messages yet - waiting for global broadcasts...&lt;/div&gt;&#39;;\n\n      console.log(`🌍 Global Listener #${this._id}: Cleared message history`);\n    });\n  }\n\n  disconnectedCallback() {\n    console.log(`🌍 Global Listener #${this._id}: Disconnecting and cleaning up!`);\n\n    // Unsubscribe from global events\n    EventBus.removeEventListener(&#39;global-message&#39;, this._boundHandlers.message);\n    EventBus.removeEventListener(&#39;global-announcement&#39;, this._boundHandlers.announcement);\n    EventBus.removeEventListener(&#39;theme-change&#39;, this._boundHandlers.theme);\n  }}\n\n\n// Register the components\ncustomElements.define(&#39;global-sender&#39;, GlobalSender);\ncustomElements.define(&#39;global-listener&#39;, GlobalListener);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<p><strong>Pro tip:</strong> This pattern works great for notifications, theme changes, or user authentication status that many components need to know about!</p>\n<hr />\n<p>Each action demonstrates a different aspect of Web Components communication!</p>\n<p>Check out the live demo below, and explore the code to see how it implements the concepts we've learned.</p>\n<h2 id=\"common-pitfalls-to-avoid\">Common Pitfalls to Avoid<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#common-pitfalls-to-avoid\" aria-label=\"Link to this section\">#</a></h2>\n<ol>\n<li>\n<p><strong>Forgetting to clean up resources</strong> in <code>disconnectedCallback()</code> - This is the #1 cause of memory leaks!</p>\n</li>\n<li>\n<p><strong>Not using Shadow DOM</strong> when appropriate - Without it, your CSS might leak out or be affected by the parent page</p>\n</li>\n<li>\n<p><strong>Overusing attributes</strong> for complex data - Consider properties instead for objects and arrays</p>\n</li>\n<li>\n<p><strong>Ignoring browser compatibility</strong> - Custom elements have worked in every current browser since January 2020 (per MDN), so you only need <a href=\"https://caniuse.com/#feat=custom-elementsv1\">Can I Use</a> and a polyfill if you support older ones</p>\n</li>\n<li>\n<p><strong>Creating too many small components</strong> - Every component has overhead; find the right balance for your application</p>\n</li>\n</ol>\n<h2 id=\"expert-tips\">Expert Tips<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#expert-tips\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li>\n<p><strong>Debug attribute changes</strong> by adding a console.log in attributeChangedCallback</p>\n</li>\n<li>\n<p><strong>Create a base component class</strong> that handles common functionality for all your components</p>\n</li>\n<li>\n<p><strong>Document your components</strong> with clear API comments - your future self will thank you!</p>\n</li>\n<li>\n<p><strong>Use Shadow DOM</strong> for true encapsulation by adding <code>this.attachShadow({mode: 'open'})</code> in your constructor</p>\n</li>\n<li>\n<p><strong>Use custom events</strong> for component communication rather than directly accessing other components</p>\n</li>\n</ul>\n<h2 id=\"try-it-yourself-interactive-playground\">Try It Yourself: Interactive Playground<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#try-it-yourself-interactive-playground\" aria-label=\"Link to this section\">#</a></h2>\n<p>Here is a small color-changing component to build on. Click it and it cycles through four colors. Edit the JavaScript tab, press Run, and click the box to check your work. Try the stages in order.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Challenge: show the color name</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;color-toggler&gt;&lt;/color-toggler&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class ColorToggler extends HTMLElement {\n  constructor() {\n    super();\n    // TODO (stage 2): turn each color into { hex, name }\n    this.colors = [&#39;#e91e63&#39;, &#39;#2196f3&#39;, &#39;#4caf50&#39;, &#39;#ff9800&#39;];\n    this.currentIndex = 0;\n    // Bind once, so removeEventListener later gets the very same function.\n    this.cycleColor = this.cycleColor.bind(this);\n  }\n\n  connectedCallback() {\n    this.style.cssText = &#39;display:block;padding:20px;margin:20px;color:white;&#39; +\n      &#39;border-radius:8px;cursor:pointer;user-select:none;&#39; +\n      &#39;transition:background-color 0.3s;background-color:&#39; + this.colors[0];\n    this.innerHTML = &#39;&lt;h3&gt;Click me to change color!&lt;/h3&gt;&#39;;\n    this.addEventListener(&#39;click&#39;, this.cycleColor);\n  }\n\n  cycleColor() {\n    this.currentIndex = (this.currentIndex + 1) % this.colors.length;\n    this.style.backgroundColor = this.colors[this.currentIndex];\n    // TODO (stage 1): update the &lt;h3&gt; text here\n  }\n\n  disconnectedCallback() {\n    this.removeEventListener(&#39;click&#39;, this.cycleColor);\n  }\n}\n\ncustomElements.define(&#39;color-toggler&#39;, ColorToggler);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the original article</a>.</p></figure>\n<p><strong>Stage 1.</strong> Make the heading say <code>Color 2 of 4</code> after the first click, then <code>Color 3 of 4</code>, and so on, wrapping back to <code>Color 1 of 4</code>. Expected result: the box changes color and its text changes together.</p>\n<p><strong>Stage 2.</strong> Give each color a name (Pink, Blue, Green, Orange) by turning the strings into objects, and show the name in the heading instead of the number. Expected result: the first click shows <code>Blue</code>. Careful: once <code>colors</code> holds objects, every place that reads <code>this.colors[...]</code> needs <code>.hex</code>.</p>\n<p><strong>Stage 3.</strong> Look at the <code>bind</code> line in the constructor. Remove it and use <code>this.addEventListener('click', this.cycleColor.bind(this))</code> instead. The component still works, but the cleanup in <code>disconnectedCallback</code> quietly stops working: <code>bind</code> makes a brand-new function each time, and <code>removeEventListener</code> only removes the exact function it is given. (In a test, a removed element still counted clicks with the inline <code>bind</code>, and stopped counting with the bound-once version.)</p>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>Stage 1: <code>this.querySelector('h3').textContent = ...</code> changes the heading you built in <code>connectedCallback</code>, and a template literal such as <code>`Color ${this.currentIndex + 1} of ${this.colors.length}`</code> builds the text. Stage 2: the color is now <code>this.colors[i].hex</code> and the label is <code>this.colors[i].name</code>.</p>\n</aside>\n<hr />\n<h2 id=\"additional-resources\">Additional Resources:<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#additional-resources\" aria-label=\"Link to this section\">#</a></h2>\n<ul>\n<li>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/Web_Components\">MDN Web Components Guide</a></p>\n</li>\n<li>\n<p><a href=\"https://github.com/mnichols08/WebComponentCallbacks\">GitHub Repo for this Demo</a></p>\n</li>\n<li>\n<p><a href=\"https://mnichols08.github.io/WebComponentCallbacks/\">Demo of Web Components Discussed Here</a></p>\n</li>\n</ul>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Lifecycle Callbacks & Component Communication\nCanonical: https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\nMastering Web Components Through Lifecycle Management and Inter-Component Interaction\nA custom element is a tag we define, and the browser tells it when something happens to it: it appeared on the page, it was removed, or one of its attributes changed. This post covers those moments (the lifecycle callbacks), then how components pass messages to each other.\nQuick recap if you are joining here: a custom element is a class that extends HTMLElement, registered with customElements.define('my-tag', MyClass). Shadow DOM gives it a private inside, so the page's CSS cannot leak in or out. To keep the short snippets short, most of them write straight into the element with innerHTML and no shadow root; the live demos use Shadow DOM. (Only put text you trust into innerHTML.)\nWe'll cover two things:\nLifecycle callbacks: the four hooks the browser calls on your element\nComponent communication: how components share data and news\nLifecycle Callbacks: The Heartbeat of Your Components\nImagine your Web Component as a living entity with a clear lifecycle: it's born, grows, changes, and eventually leaves the page. Each of these stages triggers specific callback methods that you can tap into:\n1. connectedCallback(): The Birthday Party\nWhen your component joins the DOM, it's time to celebrate! This is where initialization happens:\nclass BirthdayComponent extends HTMLElement {\nconnectedCallback() {\nconsole.log('🎉 I\\'m alive! Time to party!');\nthis.innerHTML = `\n<div class=\"birthday-card\">\n<h2>Hello World!</h2>\n<p>I was born at ${new Date().toLocaleTimeString()}</p>\n</div>\n`;\n// Start any necessary timers, fetch data, etc.\n}\n}\ncustomElements.define('birthday-card', BirthdayComponent);\nTry it yourself: Add <birthday-card></birthday-card> to your HTML and watch it come to life in the browser console!\nHTML\n<div class=\"tab-content active\" id=\"birthday-tab\" role=\"tabpanel\" aria-labelledby=\"tab-birthday\">\n<div class=\"controls\">\n<button id=\"add-birthday\">Add Birthday Component</button>\n<button id=\"remove-all-birthday\" class=\"remove\">Remove All Birthday Components</button>\n</div>\n<div id=\"birthday-container\" class=\"components-container\"></div>\n</div>\nCSS\n/* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0,0,0,0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4CAF50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196F3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9C27B0;\n--btn-mood-hover: #7B1FA2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #FF9800;\n--console-cleanup: #2196F3;\n--console-mood: #E91E63;\n--console-adopted: #8bc34a;\n--console-communication: #4CAF50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0,0,0,0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5CDF60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n/* Debug controls styling */\nbody {\nfont-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: var(--card-bg);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px var(--shadow-color);\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\n#components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n.tab {\npadding: 10px 20px;\nbackground-color: var(--highlight-color);\ncursor: pointer;\nborder-top-left-radius: 5px;\nborder-top-right-radius: 5px;\nmargin-right: 5px;\nposition: relative;\noutline: none;\nborder: none;\ncolor: var(--text-color);\ntransition: background-color 0.3s;\n}\n.tab.active {\nbackground-color: var(--card-bg);\nfont-weight: bold;\n}\n.tab:hover {\nbackground-color: var(--card-bg);\n}\n.tab:focus-visible {\nbox-shadow: 0 0 0 3px #4d90fe;\nz-index: 1;\n}\n.tab-content {\ndisplay: none;\n}\n.tab-content.active {\ndisplay: block;\n}\n.components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n/* Communication tab styles */\nh1, h2, h3, h4, h5, h6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n/* Class for elements that need to be monitored for contrast */\nJavaScript\n// Define the BirthdayComponent\nexport class BirthdayComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._colors = [\n'#FF5252', '#FF4081', '#E040FB', '#7C4DFF',\n'#536DFE', '#448AFF', '#40C4FF', '#18FFFF',\n'#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41',\n'#FFFF00', '#FFD740', '#FFAB40', '#FF6E40'];\nthis._color = this._colors[Math.floor(Math.random() * this._colors.length)];\nthis._confetti = ['🎉', '🎈', '🎊', '✨', '🎁', '🎂', '🍰'];\n}\nconnectedCallback() {\nconsole.log(`🎂 Birthday Component #${this._id}: I'm alive! Time to party!`);\n// Create and attach Shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Create the component's HTML\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 280px;\ntransition: all 0.3s ease;\n}\n.birthday-card {\nbackground-color: ${this._color};\nborder-radius: 10px;\nbox-shadow: 0 4px 8px rgba(0,0,0,0.1);\npadding: 20px;\ncolor: white;\ntext-align: center;\nposition: relative;\noverflow: hidden;\ntransition: transform 0.3s ease;\n}\n.birthday-card:hover {\ntransform: translateY(-5px) scale(1.02);\nbox-shadow: 0 6px 12px rgba(0,0,0,0.15);\n}\nh2 {\nmargin-top: 0;\nfont-size: 1.5em;\n}\n.confetti {\nposition: absolute;\nfont-size: 1.5em;\nanimation: fall 3s linear infinite;\nopacity: 0;\n}\n.button {\nbackground-color: rgba(255, 255, 255, 0.25);\nborder: none;\ncolor: white;\npadding: 8px 16px;\nborder-radius: 20px;\nmargin-top: 10px;\ncursor: pointer;\nfont-weight: bold;\ntransition: background-color 0.3s;\n}\n.button:hover {\nbackground-color: rgba(255, 255, 255, 0.4);\n}\n.remove-btn {\nposition: absolute;\ntop: 10px;\nright: 10px;\nbackground: rgba(255, 255, 255, 0.3);\nborder: none;\ncolor: white;\nwidth: 24px;\nheight: 24px;\nborder-radius: 50%;\nfont-weight: bold;\ncursor: pointer;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n@keyframes fall {\n0% {\ntransform: translateY(-20px);\nopacity: 1;\n}\n80% {\nopacity: 1;\n}\n100% {\ntransform: translateY(300px);\nopacity: 0;\n}\n}\n</style>\n<div class=\"birthday-card\">\n<button class=\"remove-btn\">×</button>\n<h2>Hello World!</h2>\n<p>I was born at ${new Date().toLocaleTimeString()}</p>\n<p>Component ID: #${this._id}</p>\n<button class=\"button\">Celebrate!</button>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('.button').addEventListener('click', () => this.celebrate());\nthis.shadowRoot.querySelector('.remove-btn').addEventListener('click', () => this.remove());\n// Start our periodic activity\nthis._activityInterval = setInterval(() => {\nconsole.log(`Birthday Component #${this._id}: Still celebrating at ${new Date().toLocaleTimeString()}`);\n}, 5000);\n}\ncelebrate() {\nconsole.log(`Birthday Component #${this._id}: Woohoo! Extra celebration!`);\n// Create confetti\nfor (let i = 0; i < 15; i++) {\nthis.addConfetti();\n}\n// Change background color\nconst newColor = this._colors[Math.floor(Math.random() * this._colors.length)];\nthis.shadowRoot.querySelector('.birthday-card').style.backgroundColor = newColor;\n}\naddConfetti() {\nconst confetti = document.createElement('span');\nconfetti.classList.add('confetti');\nconfetti.textContent = this._confetti[Math.floor(Math.random() * this._confetti.length)];\nconfetti.style.left = `${Math.random() * 100}%`;\nconfetti.style.animationDuration = `${2 + Math.random() * 3}s`;\nconfetti.style.animationDelay = `${Math.random() * 0.5}s`;\nthis.shadowRoot.querySelector('.birthday-card').appendChild(confetti);\n// Remove the confetti after animation completes\nsetTimeout(() => {\nif (confetti.parentNode) {\nconfetti.parentNode.removeChild(confetti);\n}\n}, 5000);\n}\ndisconnectedCallback() {\nconsole.log(`💫 Birthday Component #${this._id}: It's been a pleasure serving you! Cleaning up...`);\n// Clear our interval to prevent memory leaks\nif (this._activityInterval) {\nclearInterval(this._activityInterval);\n}\n// Remove any event listeners if needed\nconst button = this.shadowRoot?.querySelector('.button');\nif (button) {\nbutton.removeEventListener('click', this.celebrate);\n}\nconst removeBtn = this.shadowRoot?.querySelector('.remove-btn');\nif (removeBtn) {\nremoveBtn.removeEventListener('click', () => this.remove());\n}\n}}\n// Register the component\ncustomElements.define('birthday-card', BirthdayComponent);\n// UI Event Handlers for Birthday Components\ndocument.getElementById('add-birthday').addEventListener('click', () => {\nconst container = document.getElementById('birthday-container');\nconst component = document.createElement('birthday-card');\ncontainer.appendChild(component);\n});\ndocument.getElementById('remove-all-birthday').addEventListener('click', () => {\nconst container = document.getElementById('birthday-container');\nwhile (container.firstChild) {\ncontainer.removeChild(container.firstChild);\n}\n});\nConnected Callback Web Component Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\n2. disconnectedCallback(): The Farewell Tour\nAll good things come to an end. When your component is removed from the DOM, use this opportunity for a proper cleanup:\nclass CleanupComponent extends HTMLElement {\nconnectedCallback() {\nthis.intervalId = setInterval(() => {\nconsole.log('Still here...');\n}, 1000);\n}\ndisconnectedCallback() {\nconsole.log('💫 It\\'s been a pleasure serving you! Cleaning up...');\nclearInterval(this.intervalId);\n// Remove event listeners, close connections, etc.\n}\n}\ncustomElements.define('cleanup-demo', CleanupComponent);\nHTML\n<div aria-labelledby=\"tab-cleanup\">\n<div class=\"controls\">\n<button id=\"add-cleanup\" class=\"cleanup\">Add Farewell Tour Component</button>\n<button id=\"remove-all-cleanup\" class=\"remove\">Remove All Farewell Components</button>\n</div>\n<div id=\"cleanup-container\" class=\"components-container\"></div>\n</div>\nCSS\n/* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0, 0, 0, 0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4caf50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196f3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9c27b0;\n--btn-mood-hover: #7b1fa2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #ff9800;\n--console-cleanup: #2196f3;\n--console-mood: #e91e63;\n--console-adopted: #8bc34a;\n--console-communication: #4caf50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0, 0, 0, 0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5cdf60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n/* Debug controls styling */\nbody {\nfont-family: \"Segoe UI\", Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: var(--card-bg);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px var(--shadow-color);\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.cleanup {\nbackground-color: var(--btn-cleanup);\n}\nbutton.cleanup:hover {\nbackground-color: var(--btn-cleanup-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\n#components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n.tab {\npadding: 10px 20px;\nbackground-color: var(--highlight-color);\ncursor: pointer;\nborder-top-left-radius: 5px;\nborder-top-right-radius: 5px;\nmargin-right: 5px;\nposition: relative;\noutline: none;\nborder: none;\ncolor: var(--text-color);\ntransition: background-color 0.3s;\n}\n.tab:hover {\nbackground-color: var(--card-bg);\n}\n.tab:focus-visible {\nbox-shadow: 0 0 0 3px #4d90fe;\nz-index: 1;\n}\n.components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n/* Communication tab styles */\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n/* Class for elements that need to be monitored for contrast */\nJavaScript\n// Define the CleanupComponent (Farewell Tour)\nexport class CleanupComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._countdown = 10; // 10 second countdown\nthis._colors = [\"#3F51B5\", \"#2196F3\", \"#03A9F4\", \"#00BCD4\", \"#009688\"];\nthis._color = this._colors[Math.floor(Math.random() * this._colors.length)];\n}\nconnectedCallback() {\nconsole.log(`🔄 Cleanup Component #${this._id}: Starting my farewell tour!`);\n// Create and attach Shadow DOM\nthis.attachShadow({ mode: \"open\" });\n// Create the component's HTML\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 280px;\ntransition: all 0.3s ease;\n}\n.cleanup-card {\nbackground-color: ${this._color};\nborder-radius: 10px;\nbox-shadow: 0 4px 8px rgba(0,0,0,0.1);\npadding: 20px;\ncolor: white;\ntext-align: center;\nposition: relative;\n}\nh2 {\nmargin-top: 0;\nfont-size: 1.5em;\n}\n.timer {\nfont-size: 2em;\nfont-weight: bold;\nmargin: 10px 0;\n}\n.progress-bar {\nheight: 10px;\nbackground-color: rgba(255, 255, 255, 0.3);\nborder-radius: 5px;\nmargin: 15px 0;\noverflow: hidden;\n}\n.progress {\nheight: 100%;\nbackground-color: white;\nwidth: 100%;\ntransition: width 1s linear;\n}\n.button {\nbackground-color: rgba(255, 255, 255, 0.25);\nborder: none;\ncolor: white;\npadding: 8px 16px;\nborder-radius: 20px;\nmargin-top: 10px;\ncursor: pointer;\nfont-weight: bold;\ntransition: background-color 0.3s;\n}\n.button:hover {\nbackground-color: rgba(255, 255, 255, 0.4);\n}\n.remove-btn {\nposition: absolute;\ntop: 10px;\nright: 10px;\nbackground: rgba(255, 255, 255, 0.3);\nborder: none;\ncolor: white;\nwidth: 24px;\nheight: 24px;\nborder-radius: 50%;\nfont-weight: bold;\ncursor: pointer;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n@keyframes pulse {\n0% { transform: scale(1); }\n50% { transform: scale(1.05); }\n100% { transform: scale(1); }\n}\n.farewell {\nanimation: pulse 1s infinite;\n}\n</style>\n<div class=\"cleanup-card\">\n<button class=\"remove-btn\">×</button>\n<h2>Farewell Tour</h2>\n<p>Component ID: #${this._id}</p>\n<p>I'll self-destruct in:</p>\n<div class=\"timer\">${this._countdown}</div>\n<div class=\"progress-bar\">\n<div class=\"progress\"></div>\n</div>\n<button class=\"button\">Reset Timer</button>\n</div>\n`;\n// Start our periodic activity - this will be cleaned up in disconnectedCallback\nthis._intervalId = setInterval(() => {\nconsole.log(\n`Cleanup Component #${this._id}: Still here... ${this._countdown} seconds remaining`);\n}, 1000);\n// Set up countdown\nthis.startCountdown();\n// Add event listeners\nthis.shadowRoot.\nquerySelector(\".button\").\naddEventListener(\"click\", () => this.resetTimer());\nthis.shadowRoot.\nquerySelector(\".remove-btn\").\naddEventListener(\"click\", () => this.remove());\n}\nstartCountdown() {\n// Reset the progress bar\nconst progressBar = this.shadowRoot.querySelector(\".progress\");\nprogressBar.style.width = \"100%\";\n// Start the countdown timer\nthis._countdownTimer = setInterval(() => {\nthis._countdown--;\nthis.shadowRoot.querySelector(\".timer\").textContent = this._countdown;\n// Update progress bar\nconst percentage = this._countdown / 10 * 100;\nprogressBar.style.width = `${percentage}%`;\nif (this._countdown <= 3) {\nthis.shadowRoot.querySelector(\".cleanup-card\").classList.add(\"farewell\");\n}\nif (this._countdown <= 0) {\n// Self-destruct when countdown reaches zero\nclearInterval(this._countdownTimer);\nconsole.log(\n`👋 Cleanup Component #${this._id}: Time's up! Initiating self-destruction...`);\n// Remove after a short delay to show 0\nsetTimeout(() => {\nthis.remove();\n}, 500);\n}\n}, 1000);\n}\nresetTimer() {\nconsole.log(`Cleanup Component #${this._id}: Timer reset to 10 seconds!`);\n// Clear existing timer\nclearInterval(this._countdownTimer);\n// Reset countdown and UI\nthis._countdown = 10;\nthis.shadowRoot.querySelector(\".timer\").textContent = this._countdown;\nthis.shadowRoot.querySelector(\".cleanup-card\").classList.remove(\"farewell\");\n// Start a new countdown\nthis.startCountdown();\n}\ndisconnectedCallback() {\nconsole.log(\n`💫 Cleanup Component #${this._id}: It's been a pleasure serving you! Cleaning up...`);\n// Clear our intervals to prevent memory leaks\nif (this._intervalId) {\nclearInterval(this._intervalId);\n}\nif (this._countdownTimer) {\nclearInterval(this._countdownTimer);\n}\n// Remove any event listeners\nconst resetButton = this.shadowRoot?.querySelector(\".button\");\nif (resetButton) {\nresetButton.removeEventListener(\"click\", () => this.resetTimer());\n}\nconst removeBtn = this.shadowRoot?.querySelector(\".remove-btn\");\nif (removeBtn) {\nremoveBtn.removeEventListener(\"click\", () => this.remove());\n}\n}}\n// Register the component\ncustomElements.define(\"cleanup-demo\", CleanupComponent);\n// UI Event Handlers for Cleanup Components\ndocument.getElementById(\"add-cleanup\").addEventListener(\"click\", () => {\nconst container = document.getElementById(\"cleanup-container\");\nconst component = document.createElement(\"cleanup-demo\");\ncontainer.appendChild(component);\n});\ndocument.getElementById(\"remove-all-cleanup\").addEventListener(\"click\", () => {\nconst container = document.getElementById(\"cleanup-container\");\nwhile (container.firstChild) {\ncontainer.removeChild(container.firstChild);\n}\n});\nDisconnected Callback Web Component Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nHands-on challenge: Create a component with a button that removes itself after a countdown. Watch the disconnectedCallback fire in your console!\n3. attributeChangedCallback(name, oldValue, newValue): The Chameleon Effect\nComponents need to react to changes. This callback is your component's sense organ:\nclass MoodRing extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['mood'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'mood') {\nconsole.log(`🔄 Mood changing from ${oldValue || 'unset'} to ${newValue}!`);\n// Update the UI based on the new mood\nconst moodEmojis = {\nhappy: '😀',\nsad: '😢',\nangry: '😠',\nconfused: '🤔'\n};\nthis.innerHTML = `\n<div class=\"mood-display\">\n<span style=\"font-size: 3rem\">${moodEmojis[newValue] || '😐'}</span>\n<p>I'm feeling ${newValue || 'neutral'}!</p>\n</div>\n`;\n}\n}\n}\ncustomElements.define('mood-ring', MoodRing);\nA word on the observedAttributes list, because it is the part that surprises people. An element can have any number of attributes (class, id, style, data-anything), and most of them are none of your component's business. So the browser only calls attributeChangedCallback for the names you list in static get observedAttributes(). It reads that list once, when you call customElements.define(). Leave 'mood' off the list and setAttribute('mood', 'sad') still changes the attribute, but your callback never hears about it.\nNotice oldValue too: it is null the first time, when the attribute goes from absent to present, which is why the example prints oldValue || 'unset'.\nInteractive example: Click the buttons below to change my mood!\nHTML\n<div>\n<div class=\"controls\">\n<button id=\"add-mood\" class=\"mood\">Add Mood Ring Component</button>\n<button id=\"remove-all-mood\" class=\"remove\">Remove All Mood Components</button>\n<div class=\"mood-buttons\">\n<p>Change all moods to:</p>\n<button class=\"mood-button happy\" data-mood=\"happy\">Happy</button>\n<button class=\"mood-button sad\" data-mood=\"sad\">Sad</button>\n<button class=\"mood-button angry\" data-mood=\"angry\">Angry</button>\n<button class=\"mood-button confused\" data-mood=\"confused\">Confused</button>\n</div>\n</div>\n<div id=\"mood-container\" class=\"components-container\"></div>\n</div>\nCSS\n/* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0,0,0,0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4CAF50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196F3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9C27B0;\n--btn-mood-hover: #7B1FA2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #FF9800;\n--console-cleanup: #2196F3;\n--console-mood: #E91E63;\n--console-adopted: #8bc34a;\n--console-communication: #4CAF50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0,0,0,0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5CDF60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n/* Debug controls styling */\nbody {\nfont-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: var(--card-bg);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px var(--shadow-color);\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.mood {\nbackground-color: var(--btn-mood);\n}\nbutton.mood:hover {\nbackground-color: var(--btn-mood-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\n.mood-buttons {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 5px;\nmargin-top: 10px;\n}\n.mood-button {\npadding: 8px 12px;\nborder-radius: 4px;\nborder: none;\ncursor: pointer;\nfont-weight: bold;\ncolor: white;\n}\n.happy { background-color: #FFC107; }\n.sad { background-color: #2196F3; }\n.angry { background-color: #F44336; }\n.confused { background-color: #9C27B0; }\n#components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n.components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n/* Communication tab styles */\nh1, h2, h3, h4, h5, h6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n/* Class for elements that need to be monitored for contrast */\nJavaScript\n// Define the MoodRing Component (Chameleon Effect)\nexport class MoodRing extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._boundEventHandlers = {}; // Store bound event handlers for proper removal\n}\n// Define which attributes to observe\nstatic get observedAttributes() {\nreturn ['mood'];\n}\nconnectedCallback() {\nconsole.log(`😊 Mood Ring Component #${this._id}: I'm ready to express my feelings!`);\n// Create and attach Shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Set initial mood if not set\nif (!this.hasAttribute('mood')) {\nthis.setAttribute('mood', 'neutral');\n} else {\n// Force a render if mood was already set\nthis.renderMood(this.getAttribute('mood'));\n}\n}\n// Called when observed attributes change - this is the \"Chameleon Effect\"\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'mood' && newValue) {\nconsole.log(`🔄 Mood Ring Component #${this._id}: Mood changing from ${oldValue || 'unset'} to ${newValue}!`);\nthis.renderMood(newValue);\n}\n}\nrenderMood(mood) {\n// Define mood properties\nconst moodEmojis = {\nhappy: '😀',\nsad: '😢',\nangry: '😠',\nconfused: '🤔',\nneutral: '😐' };\nconst moodColors = {\nhappy: '#FFC107',\nsad: '#2196F3',\nangry: '#F44336',\nconfused: '#9C27B0',\nneutral: '#607D8B' };\nconst moodAnimations = {\nhappy: 'bounce 0.5s infinite alternate',\nsad: 'droop 2s infinite',\nangry: 'shake 0.5s infinite',\nconfused: 'spin 3s infinite',\nneutral: 'none' };\nconst moodDescriptions = {\nhappy: 'I feel great today! Everything is awesome!',\nsad: 'Feeling blue... could use a hug.',\nangry: 'Grrr! Something\\'s really bothering me!',\nconfused: 'Wait, what? I\\'m not sure what\\'s happening...',\nneutral: 'Just another day, nothing special.' };\n// Ensure we have a valid mood\nconst currentMood = mood in moodEmojis ? mood : 'neutral';\nconst emoji = moodEmojis[currentMood];\nconst color = moodColors[currentMood];\nconst animation = moodAnimations[currentMood];\nconst description = moodDescriptions[currentMood];\n// Create the component's HTML\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 280px;\ntransition: all 0.3s ease;\n}\n.mood-card {\nbackground-color: ${color};\nborder-radius: 10px;\nbox-shadow: 0 4px 8px rgba(0,0,0,0.1);\npadding: 20px;\ncolor: white;\ntext-align: center;\nposition: relative;\n}\nh2 {\nmargin-top: 0;\nfont-size: 1.5em;\n}\n.emoji {\nfont-size: 5em;\nmargin: 15px 0;\ndisplay: inline-block;\nanimation: ${animation};\n}\n.mood-buttons {\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: center;\ngap: 5px;\nmargin-top: 15px;\n}\n.mood-button {\nbackground-color: rgba(255, 255, 255, 0.25);\nborder: none;\ncolor: white;\npadding: 8px 12px;\nborder-radius: 20px;\ncursor: pointer;\nfont-weight: bold;\ntransition: background-color 0.3s;\n}\n.mood-button:hover {\nbackground-color: rgba(255, 255, 255, 0.4);\n}\n.remove-btn {\nposition: absolute;\ntop: 10px;\nright: 10px;\nbackground: rgba(255, 255, 255, 0.3);\nborder: none;\ncolor: white;\nwidth: 24px;\nheight: 24px;\nborder-radius: 50%;\nfont-weight: bold;\ncursor: pointer;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n@keyframes bounce {\n0% { transform: translateY(0); }\n100% { transform: translateY(-10px); }\n}\n@keyframes droop {\n0% { transform: rotate(0deg); }\n50% { transform: rotate(-5deg); }\n100% { transform: rotate(0deg); }\n}\n@keyframes shake {\n0% { transform: translateX(0); }\n25% { transform: translateX(-5px); }\n50% { transform: translateX(0); }\n75% { transform: translateX(5px); }\n100% { transform: translateX(0); }\n}\n@keyframes spin {\n0% { transform: rotate(0deg); }\n100% { transform: rotate(360deg); }\n}\n</style>\n<div class=\"mood-card\">\n<button class=\"remove-btn\">×</button>\n<h2>Mood Ring #${this._id}</h2>\n<div class=\"emoji\">${emoji}</div>\n<p>I'm feeling <strong>${currentMood}</strong>!</p>\n<p>${description}</p>\n<div class=\"mood-buttons\">\n<button class=\"mood-button\" data-mood=\"happy\">Happy</button>\n<button class=\"mood-button\" data-mood=\"sad\">Sad</button>\n<button class=\"mood-button\" data-mood=\"angry\">Angry</button>\n<button class=\"mood-button\" data-mood=\"confused\">Confused</button>\n<button class=\"mood-button\" data-mood=\"neutral\">Neutral</button>\n</div>\n</div>\n`;\n// Clean up old event handlers if they exist\nthis._removeEventListeners();\n// Create properly bound event handlers that we can remove later\nthis._boundEventHandlers.remove = () => this.remove();\nthis._boundEventHandlers.moodChange = e => {\nconst newMood = e.target.dataset.mood;\nif (newMood) {\nthis.setAttribute('mood', newMood);\n}\n};\n// Add event listeners\nconst removeBtn = this.shadowRoot.querySelector('.remove-btn');\nif (removeBtn) {\nremoveBtn.addEventListener('click', this._boundEventHandlers.remove);\n}\n// Add mood button listeners with properly bound handlers\nconst moodButtons = this.shadowRoot.querySelectorAll('.mood-button');\nif (moodButtons) {\nmoodButtons.forEach(button => {\nbutton.addEventListener('click', this._boundEventHandlers.moodChange);\n});\n}\n}\n_removeEventListeners() {\n// Remove the event listeners if element exists and handler is defined\nconst removeBtn = this.shadowRoot?.querySelector('.remove-btn');\nif (removeBtn && this._boundEventHandlers.remove) {\nremoveBtn.removeEventListener('click', this._boundEventHandlers.remove);\n}\nconst moodButtons = this.shadowRoot?.querySelectorAll('.mood-button');\nif (moodButtons && this._boundEventHandlers.moodChange) {\nmoodButtons.forEach(button => {\nbutton.removeEventListener('click', this._boundEventHandlers.moodChange);\n});\n}\n}\ndisconnectedCallback() {\nconsole.log(`💫 Mood Ring Component #${this._id}: Goodbye! My final mood was ${this.getAttribute('mood')}.`);\n// Remove all event listeners\nthis._removeEventListeners();\n}}\n// Register the component\ncustomElements.define('mood-ring', MoodRing);\n// UI Event Handlers for Mood Ring Components\ndocument.getElementById('add-mood').addEventListener('click', () => {\nconst container = document.getElementById('mood-container');\nconst component = document.createElement('mood-ring');\n// Set a random initial mood\nconst moods = ['happy', 'sad', 'angry', 'confused', 'neutral'];\nconst randomMood = moods[Math.floor(Math.random() * moods.length)];\n// First append to DOM, then set attribute to ensure component is connected\ncontainer.appendChild(component);\n// Use setTimeout to ensure the component is fully initialized\nsetTimeout(() => {\ncomponent.setAttribute('mood', randomMood);\n}, 0);\n});\ndocument.getElementById('remove-all-mood').addEventListener('click', () => {\nconst container = document.getElementById('mood-container');\nwhile (container.firstChild) {\ncontainer.removeChild(container.firstChild);\n}\n});\n// Global mood change buttons - Fix event handling\nconst moodButtons = document.querySelectorAll('.mood-buttons .mood-button');\nmoodButtons.forEach(button => {\nbutton.addEventListener('click', e => {\ne.stopPropagation(); // Prevent any parent handlers from firing\nconst newMood = button.dataset.mood;\nconsole.log(`Global mood change: Setting all rings to ${newMood}`);\ndocument.querySelectorAll('mood-ring').forEach(ring => {\nring.setAttribute('mood', newMood);\n});\n});\n});\nAttribute Changed Callback Web Component Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\n<mood-ring mood=\"happy\"></mood-ring>\n<div>\n<button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'happy')\">Happy</button>\n<button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'sad')\">Sad</button>\n<button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'angry')\">Angry</button>\n<button onclick=\"document.querySelector('mood-ring').setAttribute('mood', 'confused')\">Confused</button>\n</div>\nYour turn: wake up the mood ring. This smaller mood ring is asleep. It starts with mood=\"happy\" and shows nothing, and the Sad and Angry buttons do nothing either. Fix it in two steps, pressing Run after each.\nHTML\n<mood-ring mood=\"happy\"></mood-ring>\n<p>\n<button data-mood=\"sad\">Sad</button>\n<button data-mood=\"angry\">Angry</button>\n</p>\nCSS\nbody { font-family: sans-serif; }\nmood-ring { display: block; font-size: 1.2rem; min-height: 4rem; }\nJavaScript\nclass MoodRing extends HTMLElement {\nstatic get observedAttributes() {\nreturn []; // TODO 1: list the attribute this ring should watch\n}\nattributeChangedCallback(name, oldValue, newValue) {\nconst emojis = {\nhappy: '😀',\nsad: '😢'\n// TODO 2: add an 'angry' case\n};\nthis.textContent = `${emojis[newValue] || '😐'} I'm feeling ${newValue}!`;\n}\n}\ncustomElements.define('mood-ring', MoodRing);\ndocument.querySelectorAll('button[data-mood]').forEach((button) => {\nbutton.addEventListener('click', () => {\ndocument.querySelector('mood-ring').setAttribute('mood', button.dataset.mood);\n});\n});\nChallenge: wake up the mood ring on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nExpected result after step 1: the ring says 😀 I'm feeling happy! on load, and Sad switches it to 😢 I'm feeling sad!. Before step 2, Angry shows the neutral 😐 face. After step 2 it shows 😠.\nTip:\nStep 1 is one word in the array. Step 2 is one more name: 'emoji' pair in the object (do not forget the comma after the line above it).\nQuestion: A component sets static get observedAttributes() { return ['mood']; }. You call ring.setAttribute('size', 'big'). What happens?\nattributeChangedCallback runs with name set to 'size'\nThe attribute changes, but attributeChangedCallback is not called\nThe browser throws an error because size is not listed\nAnswer: 2. The attribute changes, but attributeChangedCallback is not called\nExplanation: Only attributes named in observedAttributes trigger the callback. Any other attribute still changes normally on the element; your component just is not told.\n4. adoptedCallback(): The Adoption Papers\nThis one's a bit like finding yourself in a new family. When your element moves to a new document:\nclass AdoptedPet extends HTMLElement {\nadoptedCallback() {\nconsole.log('🏠 New home! Adjusting to my new document...');\n// Reset state for the new document context\n}\n}\ncustomElements.define('adopted-pet', AdoptedPet);\nFun fact: While less commonly used, this callback matters for applications that work with iframes or multiple documents!\nHTML\n<div>\n<div class=\"controls\">\n<button id=\"add-adopted\" class=\"adopted\">Add Adoption Papers</button>\n<button id=\"batch-adopt\" class=\"adopted-action\">Batch Adopt All Pets</button>\n<button id=\"remove-all-adopted\" class=\"remove\">Remove All Adoption Papers</button>\n</div>\n<div class=\"adoption-sections\">\n<div class=\"adoption-area\">\n<h3>Shelter</h3>\n<div id=\"adopted-container\" class=\"components-container\"></div>\n</div>\n<div class=\"adoption-area\">\n<h3>Forever Homes</h3>\n<div id=\"forever-homes-container\" class=\"components-container\"></div>\n</div>\n</div>\n</div>\nCSS\n/* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0,0,0,0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4CAF50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196F3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9C27B0;\n--btn-mood-hover: #7B1FA2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #FF9800;\n--console-cleanup: #2196F3;\n--console-mood: #E91E63;\n--console-adopted: #8bc34a;\n--console-communication: #4CAF50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0,0,0,0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5CDF60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n/* Debug controls styling */\nbody {\nfont-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: var(--card-bg);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px var(--shadow-color);\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\nbutton.adopted {\nbackground-color: var(--btn-adopted);\n}\nbutton.adopted:hover {\nbackground-color: var(--btn-adopted-hover);\n}\nbutton.adopted-action {\nbackground-color: var(--btn-primary);\n}\nbutton.adopted-action:hover {\nbackground-color: var(--btn-primary-hover);\n}\n.sad { background-color: #2196F3; }\n#components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n.components-container {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 20px;\n}\n.adoption-sections {\ndisplay: flex;\nflex-direction: column;\ngap: 30px;\n}\n.adoption-area {\nbackground-color: var(--card-bg);\nborder-radius: 8px;\npadding: 15px;\nbox-shadow: 0 2px 4px var(--shadow-color);\n}\n.adoption-area h3 {\nmargin-top: 0;\ncolor: var(--text-color);\npadding-bottom: 10px;\nborder-bottom: 1px solid var(--border-color);\nmargin-bottom: 15px;\n}\n#forever-homes-container {\nmin-height: 100px;\nbackground-color: rgba(139, 195, 74, 0.1);\nborder-radius: 8px;\npadding: 10px;\n}\n#adopted-container {\nmin-height: 100px;\n}\n/* Communication tab styles */\nh1, h2, h3, h4, h5, h6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n/* Class for elements that need to be monitored for contrast */\nJavaScript\n// Define the AdoptedPet Component (demonstrating adoptedCallback)\nexport class AdoptedPet extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._petTypes = ['🐶', '🐱', '🐰', '🐦', '🐹', '🦊', '🐢', '🦜', '🐠'];\nthis._petType = this._petTypes[Math.floor(Math.random() * this._petTypes.length)];\nthis._petNames = ['Buddy', 'Luna', 'Charlie', 'Max', 'Bella', 'Daisy', 'Rocky', 'Lucy', 'Bailey'];\nthis._petName = this._petNames[Math.floor(Math.random() * this._petNames.length)];\nthis._homes = 0; // Track how many homes this pet has been in\nthis._homeColors = ['#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', '#ff5722'];\nthis._isBeingAdopted = false; // Flag to prevent double increments\nthis._hasShadowRoot = false; // Flag to track if shadow root has been attached\n}\nconnectedCallback() {\nconsole.log(`🏡 Adoption Papers #${this._id}: ${this._petType} ${this._petName} has arrived at the shelter!`);\n// Create and attach Shadow DOM only if it hasn't been created already\nif (!this._hasShadowRoot) {\nthis.attachShadow({ mode: 'open' });\nthis._hasShadowRoot = true;\n}\n// Create our unique home ID that we'll display in the component\nif (!this._homeId) {\nthis._homeId = document.createElement('div').id || Date.now().toString(36);\n}\n// Render initial state\nthis.render();\n}\nadoptedCallback() {\n// Check if this is an actual adoption and not just a DOM reconnection\nif (!this._isBeingAdopted) {\nconsole.log(`🏠 ${this._petType} ${this._petName} (Pet #${this._id}): adoptedCallback triggered but not during adoption process. Ignoring.`);\nreturn;\n}\nthis._homes++;\nconsole.log(`🏠 ${this._petType} ${this._petName} (Pet #${this._id}): New home! This is home #${this._homes}. Adjusting to my new document...`);\n// Update home ID for the new document\nthis._homeId = document.createElement('div').id || Date.now().toString(36);\n// Render with updated home information\nthis.render();\n// Show an adoption celebration animation\nthis.celebrateAdoption();\n// Reset the adoption flag\nthis._isBeingAdopted = false;\n// After adoption, make sure component is moved to forever homes container\nsetTimeout(() => {\nconst container = document.getElementById('forever-homes-container');\nif (container && this.parentElement && this.parentElement.id !== 'forever-homes-container') {\ncontainer.appendChild(this);\n}\n}, 100);\n}\ncelebrateAdoption() {\nconst card = this.shadowRoot.querySelector('.adoption-card');\nif (card) {\ncard.classList.add('adopted');\n// Remove the animation class after animation completes\nsetTimeout(() => {\nif (card.classList.contains('adopted')) {\ncard.classList.remove('adopted');\n}\n}, 2000);\n}\n}\nrender() {\n// Only proceed if we have a shadowRoot\nif (!this.shadowRoot) return;\n// Get color based on number of homes (first color for first home, etc)\nconst colorIndex = Math.min(this._homes, this._homeColors.length - 1);\nconst cardColor = this._homeColors[colorIndex];\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 280px;\ntransition: all 0.3s ease;\n}\n.adoption-card {\nbackground-color: ${cardColor};\nborder-radius: 10px;\nbox-shadow: 0 4px 8px rgba(0,0,0,0.1);\npadding: 20px;\ncolor: white;\ntext-align: center;\nposition: relative;\noverflow: hidden;\n}\n.pet-icon {\nfont-size: 5em;\nmargin: 10px 0;\n}\nh2 {\nmargin-top: 0;\nfont-size: 1.5em;\n}\n.details {\nmargin: 15px 0;\nfont-size: 14px;\ntext-align: left;\nbackground: rgba(255, 255, 255, 0.2);\npadding: 10px;\nborder-radius: 5px;\n}\n.adoption-card.adopted {\nanimation: celebrate 2s ease-in-out;\n}\n.buttons {\ndisplay: flex;\njustify-content: space-between;\nmargin-top: 15px;\n}\n.button {\nbackground-color: rgba(255, 255, 255, 0.25);\nborder: none;\ncolor: white;\npadding: 8px 16px;\nborder-radius: 20px;\ncursor: pointer;\nfont-weight: bold;\ntransition: background-color 0.3s;\nflex: 1;\nmargin: 0 5px;\n}\n.button:hover {\nbackground-color: rgba(255, 255, 255, 0.4);\n}\n.remove-btn {\nposition: absolute;\ntop: 10px;\nright: 10px;\nbackground: rgba(255, 255, 255, 0.3);\nborder: none;\ncolor: white;\nwidth: 24px;\nheight: 24px;\nborder-radius: 50%;\nfont-weight: bold;\ncursor: pointer;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n@keyframes celebrate {\n0% { transform: scale(1); }\n10% { transform: scale(1.1); }\n20% { transform: scale(1); }\n30% { transform: scale(1.1); rotate(5deg); }\n40% { transform: scale(1); rotate(-5deg); }\n50% { transform: scale(1.05); rotate(0); }\n60% { transform: scale(1); }\n100% { transform: scale(1); }\n}\n.home-stamp {\nposition: absolute;\ntop: 10px;\nleft: 10px;\nbackground: rgba(255, 255, 255, 0.3);\nfont-size: 11px;\npadding: 3px 8px;\nborder-radius: 10px;\nfont-weight: bold;\n}\n</style>\n<div class=\"adoption-card\">\n<div class=\"home-stamp\">Home #${this._homes} (${this._homeId})</div>\n<button class=\"remove-btn\">×</button>\n<div class=\"pet-icon\">${this._petType}</div>\n<h2>${this._petName}</h2>\n<div class=\"details\">\n<p><strong>Pet ID:</strong> #${this._id}</p>\n<p><strong>Adoptions:</strong> ${this._homes} ${this._homes ? '(Previously adopted)' : '(Never adopted)'}</p>\n<p><strong>Current Home:</strong> ${this._homeId}</p>\n</div>\n<div class=\"buttons\">\n<button class=\"button adopt-btn\">Adopt Me!</button>\n<button class=\"button return-btn\">Return to Shelter</button>\n</div>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('.remove-btn').addEventListener('click', () => this.remove());\nthis.shadowRoot.querySelector('.adopt-btn').addEventListener('click', () => this.adoptPet());\nthis.shadowRoot.querySelector('.return-btn').addEventListener('click', () => this.returnToShelter());\n}\nadoptPet() {\nconsole.log(`🏠 ${this._petType} ${this._petName} (Pet #${this._id}): Being adopted to a new home!`);\n// Set the flag to indicate this is an intentional adoption\nthis._isBeingAdopted = true;\n// Create a new temporary document to trigger adoptedCallback\nconst tempDoc = document.implementation.createHTMLDocument('Adoption Agency');\n// Move this element to the temporary document to trigger adoptedCallback\ntempDoc.body.appendChild(this);\n// Move it back to the forever homes container after a short delay\nsetTimeout(() => {\nconst container = document.getElementById('forever-homes-container');\nif (container) {\ncontainer.appendChild(this);\n}\n}, 100);\n}\nreturnToShelter() {\nconsole.log(`😢 ${this._petType} ${this._petName} (Pet #${this._id}): Returned to the shelter.`);\n// Show a sad animation or effect\nconst card = this.shadowRoot.querySelector('.adoption-card');\ncard.style.opacity = '0.6';\n// Reset after a moment\nsetTimeout(() => {\ncard.style.opacity = '1';\n// Move back to shelter\nconst container = document.getElementById('adopted-container');\nif (container && this.parentElement && this.parentElement.id !== 'adopted-container') {\ncontainer.appendChild(this);\n}\n}, 1000);\n}\ndisconnectedCallback() {\nconsole.log(`💫 Adoption Papers #${this._id}: ${this._petType} ${this._petName} has left our system. Good luck!`);\n// Clean up any event listeners or timers if needed\n}}\n// Register the component\ncustomElements.define('adopted-pet', AdoptedPet);\n// UI Event Handlers for Adoption Papers Components\ndocument.getElementById('add-adopted').addEventListener('click', () => {\nconst container = document.getElementById('adopted-container');\nconst component = document.createElement('adopted-pet');\ncontainer.appendChild(component);\n});\ndocument.getElementById('remove-all-adopted').addEventListener('click', () => {\n// Clear both shelter and forever homes containers\nconst shelterContainer = document.getElementById('adopted-container');\nconst homesContainer = document.getElementById('forever-homes-container');\nwhile (shelterContainer.firstChild) {\nshelterContainer.removeChild(shelterContainer.firstChild);\n}\nwhile (homesContainer.firstChild) {\nhomesContainer.removeChild(homesContainer.firstChild);\n}\n});\ndocument.getElementById('batch-adopt').addEventListener('click', () => {\nconst pets = document.querySelectorAll('#adopted-container > adopted-pet');\nif (pets.length === 0) {\nconsole.log('No pets available to adopt! Add some first.');\nreturn;\n}\nconsole.log(`🏠 Batch adopting ${pets.length} pets to a new home!`);\n// Create a temporary document to move all pets to\nconst tempDoc = document.implementation.createHTMLDocument('Batch Adoption');\n// Set the adoption flag on all pets before moving them\npets.forEach(pet => {\npet._isBeingAdopted = true;\n});\n// Move all pets to trigger adoptedCallback for each\npets.forEach(pet => {\ntempDoc.body.appendChild(pet);\n});\n// After a short delay, return them to the forever homes container\nsetTimeout(() => {\nconst container = document.getElementById('forever-homes-container');\nif (container) {\nwhile (tempDoc.body.firstChild) {\ncontainer.appendChild(tempDoc.body.firstChild);\n}\n}\n}, 100);\n});\nAdopted Callback Web Component Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nComponent Communication: Let's Talk!\nWeb Components are fantastic at encapsulation, but isolation isn't always desirable. A component that cannot tell anyone anything is a poster on a wall. Here's how to make them communicate effectively. The short version of the rules: data goes down through attributes and properties, and news goes up through events.\n1. Parent-Child Communication: The Family Chat\nJust like in human families, parent and child components can have rich communication:\nParents → Children: Passing Down Wisdom\nParents can pass data to children through attributes or properties. Here is a first draft, with the child watching a message attribute. (Spoiler: it has a bug, and finding it is the point.)\nclass ParentComponent extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div class=\"parent\">\n<h2>I'm the parent</h2>\n<child-component message=\"Be good!\"></child-component>\n<button id=\"change-message\">Change Message</button>\n</div>\n`;\nthis.querySelector('#change-message').addEventListener('click', () => {\nconst child = this.querySelector('child-component');\nchild.setAttribute('message', 'Clean your room!');\n});\n}\n}\nclass ChildComponent extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['message'];\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'message') {\nthis.querySelector('.message').textContent = newValue;\n}\n}\nconnectedCallback() {\nthis.innerHTML = `\n<div class=\"child\" style=\"border: 1px solid blue; padding: 10px; margin: 10px 0;\">\n<h3>I'm the child</h3>\n<p>Parent says: <span class=\"message\">${this.getAttribute('message') || 'Nothing yet'}</span></p>\n<button class=\"respond\">Respond to Parent</button>\n</div>\n`;\nthis.querySelector('.respond').addEventListener('click', () => {\nthis.dispatchEvent(new CustomEvent('child-response', {\nbubbles: true,\ndetail: { response: 'Okay, I will!' }\n}));\n});\n}\n}\ncustomElements.define('parent-component', ParentComponent);\ncustomElements.define('child-component', ChildComponent);\nThat looks right, and it fails. Run the first draft below. The red message is the error the browser reports:\nHTML\n<pre id=\"report\">Waiting...</pre>\n<parent-component></parent-component>\nCSS\nbody { font-family: sans-serif; }\n#report { color: #b00020; white-space: pre-wrap; }\nJavaScript\nconst report = document.getElementById('report');\nwindow.addEventListener('error', (e) => { report.textContent = 'Error: ' + e.message; });\nclass ChildComponent extends HTMLElement {\nstatic get observedAttributes() { return ['message']; }\nattributeChangedCallback(name, oldValue, newValue) {\nthis.querySelector('.message').textContent = newValue; // line that throws\n}\nconnectedCallback() {\nthis.innerHTML = `<p>Parent says: <span class=\"message\">${this.getAttribute('message')}</span></p>`;\n}\n}\nclass ParentComponent extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `<h2>I'm the parent</h2><child-component message=\"Be good!\"></child-component>`;\n}\n}\ncustomElements.define('parent-component', ParentComponent);\ncustomElements.define('child-component', ChildComponent);\nWorked example: the first draft throws on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nThe browser reports TypeError: Cannot set properties of null (setting 'textContent'). Here is why. When the child is created with message=\"Be good!\", the browser calls attributeChangedCallback for that starting value first, and only then connectedCallback. At that moment this.innerHTML has not run, so there is no .message element and querySelector returns null. The browser reports the error and carries on, so the page still looks fine (the child's text appears once connectedCallback runs). That is the sneaky part: nothing looks broken, yet every start-up logs an error and any other code in that callback is skipped.\nThe fix is to remember that the callbacks can arrive before the component has built anything. Either guard the callback, or make sure the markup exists before it runs. The guard is the smallest change:\nattributeChangedCallback(name, oldValue, newValue) {\nif (name !== 'message') return;\nconst target = this.querySelector('.message');\nif (!target) return; // not built yet: connectedCallback will read the attribute\ntarget.textContent = newValue;\n}\nNothing is lost by skipping the early call, because connectedCallback already reads this.getAttribute('message') when it builds the markup. The live demo further down uses the same idea (it checks this.shadowRoot before touching it). Try it: paste the guard into the runnable above and the red message goes away.\nQuestion: You write <x-y a=\"1\"> in the page, and x-y is a custom element that observes a. In which order does the browser call the element's callbacks?\nconnectedCallback, then attributeChangedCallback\nconstructor, then attributeChangedCallback, then connectedCallback\nattributeChangedCallback, then constructor, then connectedCallback\nAnswer: 2. constructor, then attributeChangedCallback, then connectedCallback\nExplanation: The element is constructed first, the attributes it already has are reported next (once per observed attribute), and only then is it connected. That order is why the first draft above reads the page before connectedCallback has built it.\nChildren → Parents: Speaking Up\nChildren talk back to parents using custom events. Here is the complete ParentComponent with the listener added (the child is unchanged apart from the guard):\nclass ParentComponent extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div class=\"parent\">\n<h2>I'm the parent</h2>\n<child-component message=\"Be good!\"></child-component>\n<button id=\"change-message\">Change Message</button>\n</div>\n`;\nthis.querySelector('#change-message').addEventListener('click', () => {\nconst child = this.querySelector('child-component');\nchild.setAttribute('message', 'Clean your room!');\n});\nthis.addEventListener('child-response', (e) => {\nalert(`Child says: ${e.detail.response}`);\n});\n}\n}\nThe listener sits on the parent, yet the event is fired by the child. That works because the child created it with bubbles: true, so it travels up through the parent on its way to the document.\nTry it live: Watch the following demo to see this parent-child communication in action!\nHTML\n<div>\n<parent-component></parent-component>\n</div>\nCSS\n/* /* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0,0,0,0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4CAF50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196F3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9C27B0;\n--btn-mood-hover: #7B1FA2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #FF9800;\n--console-cleanup: #2196F3;\n--console-mood: #E91E63;\n--console-adopted: #8bc34a;\n--console-communication: #4CAF50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0,0,0,0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5CDF60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n.high-contrast {\n--bg-color: #000;\n--text-color: #fff;\n--card-bg: #222;\n--border-color: #fff;\n--highlight-color: #444;\n/* High contrast button colors */\n--btn-primary: #0080ff;\n--btn-primary-hover: #00a0ff;\n--btn-cleanup: #00c0ff;\n--btn-cleanup-hover: #00e0ff;\n--btn-mood: #ff00ff;\n--btn-mood-hover: #ff40ff;\n--btn-remove: #ff0000;\n--btn-remove-hover: #ff4040;\n--btn-adopted: #00ff00;\n--btn-adopted-hover: #40ff40;\n/* Increase text contrast */\ncolor: #fff;\nbackground-color: #000;\n}\n.high-contrast button {\nborder: 2px solid white;\n}\n.high-contrast h1,\n.high-contrast h2,\n.high-contrast h3 {\ncolor: #fff;\n}\n.high-contrast input,\n.high-contrast select {\nbackground-color: #000;\ncolor: #fff;\nborder: 2px solid #fff;\n}\n/* Debug controls styling */\nbody {\nfont-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\n/* Communication tab styles */\nh1, h2, h3, h4, h5, h6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n.auto-contrast-text {\ncolor: var(--auto-text-color, inherit);\n}\n/* Class for elements that need to be monitored for contrast */\n.contrast-monitor {\nposition: relative;\ntransition: color 0.3s ease;\n}\n*/\nJavaScript\nconst EventBus = new EventTarget();\n/**\n* Utility functions for color management and accessibility\n*/\n/**\n* Determines if a color is light or dark\n* @param {string} color - Hex color code\n* @returns {boolean} True if color is light, false if dark\n*/\nfunction isLightColor(color) {\n// Remove the hash at the start if it exists\ncolor = color.replace(/^#/, \"\");\n// Parse the color\nlet r, g, b;\nif (color.length === 3) {\n// Short hex format (e.g. #ABC)\nr = parseInt(color.charAt(0) + color.charAt(0), 16);\ng = parseInt(color.charAt(1) + color.charAt(1), 16);\nb = parseInt(color.charAt(2) + color.charAt(2), 16);\n} else if (color.length === 6) {\n// Full hex format (e.g. #AABBCC)\nr = parseInt(color.substring(0, 2), 16);\ng = parseInt(color.substring(2, 4), 16);\nb = parseInt(color.substring(4, 6), 16);\n} else {\n// Handle RGB format or other non-hex cases\ntry {\nif (color.startsWith(\"rgb\")) {\nconst rgbValues = color.match(/\\d+/g);\nif (rgbValues && rgbValues.length >= 3) {\nr = parseInt(rgbValues[0]);\ng = parseInt(rgbValues[1]);\nb = parseInt(rgbValues[2]);\n} else {\nreturn true; // Default to light if we can't parse\n}\n} else {\nreturn true; // Default to light for unknown formats\n}\n} catch (e) {\nconsole.warn(\"Could not parse color:\", color);\nreturn true; // Default to light\n}\n}\n// Calculate the perceived brightness using the YIQ formula\n// This gives better results than simply averaging RGB values\nconst yiq = (r * 299 + g * 587 + b * 114) / 1000;\n// YIQ < 128 is considered dark, >= 128 is light\nreturn yiq >= 128;\n}\n/**\n* Gets the appropriate text color (black or white) for a given background color\n* @param {string} backgroundColor - Background color in hex format\n* @returns {string} - Either \"black\" or \"white\"\n*/\nfunction getContrastTextColor(backgroundColor) {\nreturn isLightColor(backgroundColor) ? \"black\" : \"white\";\n}\n/**\n* Applies the appropriate text color to all elements within a container\n* @param {HTMLElement} container - The container element\n* @param {string} backgroundColor - Background color in hex format\n*/\nfunction applyContrastText(container, backgroundColor) {\nconst textColor = getContrastTextColor(backgroundColor);\ncontainer.style.color = textColor;\n// Also handle links and other text elements that might need contrast\nconst links = container.querySelectorAll(\"a\");\nlinks.forEach(link => {\nlink.style.color = textColor === \"white\" ? \"#9cceff\" : \"#0066cc\";\n});\n}\n// Parent-Child Communication: The Family Talk demo\nclass ParentComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._responseCount = 0;\n}\nconnectedCallback() {\nconsole.log(`👨‍👩‍👧‍👦 Parent Component #${this._id}: Setting up the family!`);\n// Create shadow DOM\nthis.attachShadow({ mode: \"open\" });\n// Initial render\nthis.render();\n// Listen for child responses\nthis.shadowRoot.addEventListener(\n\"child-response\",\nthis.handleChildResponse.bind(this));\n}\nhandleChildResponse(e) {\nthis._responseCount++;\n// Update the response display\nconst responseEl = this.shadowRoot.querySelector(\".child-responses\");\nif (responseEl) {\nresponseEl.textContent = `Child says: ${e.detail.response}`;\n// Add \"times\" indicator if multiple responses\nif (this._responseCount > 1) {\nresponseEl.textContent += ` (${this._responseCount} times)`;\n}\n// Highlight the new response\nresponseEl.classList.add(\"highlight\");\nsetTimeout(() => {\nresponseEl.classList.remove(\"highlight\");\n}, 1000);\n}\nconsole.log(\n`👨‍👩‍👧‍👦 Parent Component #${this._id}: Received response from child: \"${e.detail.response}\"`);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 100%;\nmargin: 20px 0;\n}\n.parent-card {\nbackground-color: var(--card-bg, #f8f9fa);\nborder-radius: 10px;\nborder: 2px solid #4285f4;\npadding: 15px;\nfont-family: 'Segoe UI', sans-serif;\ncolor: var(--text-color, #333);\n}\n.parent-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nmargin-bottom: 10px;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 10px;\n}\nh2 {\nmargin: 0;\ncolor: #4285f4;\n}\n.children-container {\npadding: 10px;\nbackground-color: var(--highlight-color, #f0f4f8);\nborder-radius: 8px;\n}\n.response-panel {\nmargin-top: 15px;\npadding: 10px;\nbackground-color: var(--highlight-color, #e8eaf6);\nborder-radius: 8px;\n}\n.child-responses {\npadding: 8px;\ncolor: #5c6bc0;\nfont-weight: bold;\n}\n.highlight {\nanimation: highlight 1s ease-in-out;\n}\n@keyframes highlight {\n0% {\nbackground-color: rgba(197, 202, 233, 0.5);\ncolor: ${getContrastTextColor(\"rgba(197, 202, 233, 0.5)\")};\n}\n100% {\nbackground-color: transparent;\n}\n}\nbutton {\nbackground-color: #4285f4;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 12px;\ncursor: pointer;\ntransition: background-color 0.3s;\n}\nbutton:hover {\nbackground-color: #3367d6;\n}\nbutton:focus-visible {\noutline: 3px solid #a1c4fd;\noutline-offset: 2px;\n}\n.message-input {\ndisplay: flex;\ngap: 10px;\nmargin-top: 10px;\nmargin-bottom: 10px;\n}\ninput {\nflex: 1;\npadding: 8px;\nborder: 1px solid var(--border-color, #ccc);\nborder-radius: 4px;\nbackground-color: var(--card-bg, #fff);\ncolor: var(--text-color, #333);\n}\n@media (prefers-color-scheme: dark) {\n.parent-card {\nborder-color: #5c9eff;\n}\nh2, h3 {\ncolor: #5c9eff;\n}\n.child-responses {\ncolor: #7986cb;\n}\nbutton:focus-visible {\noutline-color: #83b9ff;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .parent-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h2,\n:host-context(.high-contrast) h3 {\ncolor: white;\n}\n:host-context(.high-contrast) input {\nbackground-color: black;\ncolor: white;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button {\nbackground-color: #0080ff;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button:hover {\nbackground-color: #00a0ff;\n}\n:host-context(.high-contrast) .children-container,\n:host-context(.high-contrast) .response-panel {\nbackground-color: #333;\n}\n/* Add contrast text utilities */\n.auto-contrast-text {\ntransition: color 0.3s ease;\n}\n</style>\n<div class=\"parent-card contrast-monitor\" role=\"region\" aria-label=\"Parent component demonstration\">\n<div class=\"parent-header\">\n<h2 id=\"parent-heading-${\nthis._id\n}\" class=\"auto-contrast-text\">Parent Component #${this._id}</h2>\n<span aria-hidden=\"true\">👨‍👩‍👧‍👦</span>\n</div>\n<div class=\"message-input\">\n<input type=\"text\" id=\"message-input\"\naria-label=\"Message to send to child\"\nvalue=\"Do your homework!\">\n<button id=\"send-message\" aria-label=\"Send message to child component\">Send to Child</button>\n</div>\n<div class=\"children-container\" aria-labelledby=\"parent-heading-${\nthis._id\n}\">\n<child-component message=\"Be good!\"></child-component>\n</div>\n<div class=\"response-panel\" aria-live=\"polite\">\n<h3 class=\"auto-contrast-text\">Child Responses:</h3>\n<div class=\"child-responses\">Waiting for response...</div>\n</div>\n</div>\n`;\n// Apply contrast to the component after rendering\nthis._applyInitialContrast();\n// Add event listeners\nthis.shadowRoot.\nquerySelector(\"#send-message\").\naddEventListener(\"click\", () => {\nconst message = this.shadowRoot.querySelector(\"#message-input\").value;\nconst child = this.shadowRoot.querySelector(\"child-component\");\nchild.setAttribute(\"message\", message);\nconsole.log(\n`👨‍👩‍👧‍👦 Parent Component #${this._id}: Sent message to child: \"${message}\"`);\n});\n}\n_applyInitialContrast() {\n// Wait for styles to be computed\nsetTimeout(() => {\nconst card = this.shadowRoot.querySelector(\".parent-card\");\nconst computedStyle = getComputedStyle(card);\nconst bgColor = computedStyle.backgroundColor;\n// Determine text color based on background\nconst textColor = getContrastTextColor(bgColor);\n// Apply to auto-contrast elements\nconst textElements = card.querySelectorAll(\".auto-contrast-text\");\ntextElements.forEach(el => {\nel.style.color = textColor;\n});\n}, 0);\n}\ndisconnectedCallback() {\nconsole.log(\n`👨‍👩‍👧‍👦 Parent Component #${this._id}: Family disbanded. Cleaning up...`);\n// Clean up event listeners\nthis.shadowRoot.removeEventListener(\n\"child-response\",\nthis.handleChildResponse);\n}}\nclass ChildComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\n}\nstatic get observedAttributes() {\nreturn [\"message\"];\n}\nconnectedCallback() {\nconsole.log(`👶 Child Component #${this._id}: I'm here!`);\n// Create shadow DOM\nthis.attachShadow({ mode: \"open\" });\n// Initial render\nthis.render();\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === \"message\" && this.shadowRoot) {\nconst messageEl = this.shadowRoot.querySelector(\".parent-message\");\nif (messageEl) {\nmessageEl.textContent = newValue;\n// Add a brief highlight animation\nmessageEl.classList.add(\"new-message\");\nsetTimeout(() => {\nmessageEl.classList.remove(\"new-message\");\n}, 1000);\n}\nconsole.log(\n`👶 Child Component #${this._id}: Received message from parent: \"${newValue}\"`);\n}\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 10px 0;\n}\n.child-card {\nbackground-color: var(--card-bg, white);\nborder-radius: 8px;\nborder: 2px solid #fbbc04;\npadding: 12px;\ncolor: var(--text-color, #333);\n}\n.child-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 8px;\nmargin-bottom: 8px;\n}\nh3 {\nmargin: 0;\ncolor: #fbbc04;\n}\n.message-container {\npadding: 8px;\nbackground-color: var(--highlight-color, #fff8e1);\nborder-radius: 6px;\nmargin-bottom: 10px;\n}\n.parent-message {\nfont-weight: bold;\ncolor: #fb8c00;\n}\n.new-message {\nanimation: highlight-message 1s ease-in-out;\n}\n@keyframes highlight-message {\n0% { background-color: rgba(255, 224, 178, 0.5); }\n100% { background-color: transparent; }\n}\n.responses {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 8px;\n}\nbutton {\nbackground-color: #fbbc04;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 6px 10px;\ncursor: pointer;\ntransition: background-color 0.3s;\nfont-size: 0.9em;\n}\nbutton:hover {\nbackground-color: #f9a825;\n}\nbutton:focus-visible {\noutline: 3px solid #ffe082;\noutline-offset: 2px;\n}\n@media (prefers-color-scheme: dark) {\n.child-card {\nborder-color: #ffd54f;\n}\nh3 {\ncolor: #ffd54f;\n}\n.parent-message {\ncolor: #ffb74d;\n}\nbutton:focus-visible {\noutline-color: #ffecb3;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .child-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h3 {\ncolor: white;\n}\n:host-context(.high-contrast) .parent-message {\ncolor: white;\n}\n:host-context(.high-contrast) button {\nbackground-color: #0080ff;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button:hover {\nbackground-color: #00a0ff;\n}\n:host-context(.high-contrast) .message-container {\nbackground-color: #333;\n}\n</style>\n<div class=\"child-card\" role=\"region\" aria-label=\"Child component demonstration\">\n<div class=\"child-header\">\n<h3 id=\"child-heading-${this._id}\">Child Component #${this._id}</h3>\n<span aria-hidden=\"true\">👶</span>\n</div>\n<div class=\"message-container\" aria-live=\"polite\">\n<p>Parent says: <span class=\"parent-message\">${\nthis.getAttribute(\"message\") || \"Nothing yet\"\n}</span></p>\n</div>\n<div class=\"responses\">\n<button class=\"respond-yes\" aria-label=\"Respond with yes I will\">Respond \"Yes, I will!\"</button>\n<button class=\"respond-no\" aria-label=\"Respond with not now\">Respond \"Not now!\"</button>\n</div>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.\nquerySelector(\".respond-yes\").\naddEventListener(\"click\", () => {\nthis.dispatchEvent(\nnew CustomEvent(\"child-response\", {\nbubbles: true,\ncomposed: true, // this is needed to cross shadow DOM boundaries\ndetail: { response: \"Yes, I will!\" } }));\n});\nthis.shadowRoot.querySelector(\".respond-no\").addEventListener(\"click\", () => {\nthis.dispatchEvent(\nnew CustomEvent(\"child-response\", {\nbubbles: true,\ncomposed: true,\ndetail: { response: \"Not now!\" } }));\n});\n}\ndisconnectedCallback() {\nconsole.log(`👶 Child Component #${this._id}: Goodbye!`);\n}}\n// Register the components\ncustomElements.define(\"parent-component\", ParentComponent);\ncustomElements.define(\"child-component\", ChildComponent);\nParent / Child Communication Web Components Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\n2. Sibling Communication: The Playground Talk\nSometimes components at the same level need to coordinate:\nclass SiblingContainer extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div class=\"container\">\n<h2>Sibling Communication</h2>\n<sibling-sender></sibling-sender>\n<sibling-receiver></sibling-receiver>\n</div>\n`;\n// The parent mediates communication between siblings\nthis.addEventListener('message-sent', (e) => {\nconst receiver = this.querySelector('sibling-receiver');\nreceiver.receiveMessage(e.detail.message);\n});\n}\n}\nclass SiblingSender extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div style=\"border: 1px solid green; padding: 10px; margin: 10px 0;\">\n<h3>Sibling A (Sender)</h3>\n<input type=\"text\" placeholder=\"Type a message\" value=\"Hello sibling!\">\n<button>Send to Sibling B</button>\n</div>\n`;\nthis.querySelector('button').addEventListener('click', () => {\nconst message = this.querySelector('input').value;\nthis.dispatchEvent(new CustomEvent('message-sent', {\nbubbles: true,\ndetail: { message }\n}));\n});\n}\n}\nclass SiblingReceiver extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div style=\"border: 1px solid purple; padding: 10px; margin: 10px 0;\">\n<h3>Sibling B (Receiver)</h3>\n<p>Message received: <span class=\"message\">Nothing yet</span></p>\n</div>\n`;\n}\nreceiveMessage(message) {\nthis.querySelector('.message').textContent = message;\n// Add animation to show the message arrived\nconst messageElement = this.querySelector('.message');\nmessageElement.style.animation = 'highlight 1s';\nsetTimeout(() => { messageElement.style.animation = ''; }, 1000);\n}\n}\ncustomElements.define('sibling-container', SiblingContainer);\ncustomElements.define('sibling-sender', SiblingSender);\ncustomElements.define('sibling-receiver', SiblingReceiver);\nHTML\n<div>\n<sibling-container></sibling-container>\n</div>\nCSS\n/* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0,0,0,0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4CAF50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196F3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9C27B0;\n--btn-mood-hover: #7B1FA2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #FF9800;\n--console-cleanup: #2196F3;\n--console-mood: #E91E63;\n--console-adopted: #8bc34a;\n--console-communication: #4CAF50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0,0,0,0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5CDF60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n.high-contrast {\n--bg-color: #000;\n--text-color: #fff;\n--card-bg: #222;\n--border-color: #fff;\n--highlight-color: #444;\n/* High contrast button colors */\n--btn-primary: #0080ff;\n--btn-primary-hover: #00a0ff;\n--btn-cleanup: #00c0ff;\n--btn-cleanup-hover: #00e0ff;\n--btn-mood: #ff00ff;\n--btn-mood-hover: #ff40ff;\n--btn-remove: #ff0000;\n--btn-remove-hover: #ff4040;\n--btn-adopted: #00ff00;\n--btn-adopted-hover: #40ff40;\n/* Increase text contrast */\ncolor: #fff;\nbackground-color: #000;\n}\n.high-contrast button {\nborder: 2px solid white;\n}\n.high-contrast h1,\n.high-contrast h2,\n.high-contrast h3 {\ncolor: #fff;\n}\n.high-contrast input,\n.high-contrast select {\nbackground-color: #000;\ncolor: #fff;\nborder: 2px solid #fff;\n}\n/* Debug controls styling */\nbody {\nfont-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: var(--card-bg);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px var(--shadow-color);\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\n/* Communication tab styles */\nh1, h2, h3, h4, h5, h6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n/* Class for elements that need to be monitored for contrast */\nJavaScript\n// Sibling Communication: Components that talk through a parent/container\nexport class SiblingContainer extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\n}\nconnectedCallback() {\nconsole.log(`🏠 Sibling Container #${this._id}: Setting up the playground!`);\n// Create shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Initial render\nthis.render();\n// Listen for and facilitate messages between siblings\nthis.shadowRoot.addEventListener('message-sent', this.relayMessage.bind(this));\n}\nrelayMessage(e) {\nconst message = e.detail.message;\nconst sender = e.detail.sender;\n// Find all receivers and relay the message\nconst receivers = this.shadowRoot.querySelectorAll('sibling-receiver');\nreceivers.forEach(receiver => {\nreceiver.receiveMessage(message, sender);\n});\nconsole.log(`🏠 Sibling Container #${this._id}: Relayed message \"${message}\" from Sibling ${sender}`);\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nwidth: 100%;\nmargin: 20px 0;\n}\n.container-card {\nbackground-color: var(--card-bg, #f8f9fa);\nborder-radius: 10px;\nborder: 2px solid #34a853;\npadding: 15px;\nfont-family: 'Segoe UI', sans-serif;\ncolor: var(--text-color, #333);\n}\n.container-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nmargin-bottom: 15px;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 10px;\n}\nh2 {\nmargin: 0;\ncolor: #34a853;\n}\n.siblings-layout {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 15px;\n}\n@media (min-width: 768px) {\n.siblings-layout {\nflex-direction: row;\n}\n.sibling-column {\nflex: 1;\n}\n}\n@media (max-width: 767px) {\n.siblings-layout {\nflex-direction: column;\n}\n}\n.sibling-column {\nmin-width: 200px;\n}\nbutton {\nbackground-color: #34a853;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 12px;\ncursor: pointer;\ntransition: background-color 0.3s;\n}\nbutton:hover {\nbackground-color: #2e7d32;\n}\nbutton:focus-visible {\noutline: 3px solid #a5d6a7;\noutline-offset: 2px;\n}\n@media (prefers-color-scheme: dark) {\n.container-card {\nborder-color: #4caf50;\n}\nh2 {\ncolor: #4caf50;\n}\nbutton:focus-visible {\noutline-color: #81c784;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .container-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h2 {\ncolor: white;\n}\n:host-context(.high-contrast) input {\nbackground-color: black;\ncolor: white;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button {\nbackground-color: #0080ff;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) .messages-panel {\nbackground-color: #333;\n}\n:host-context(.high-contrast) .message {\nborder: 1px solid #666;\n}\n</style>\n<div class=\"container-card\" role=\"region\" aria-labelledby=\"container-heading-${this._id}\">\n<div class=\"container-header\">\n<h2 id=\"container-heading-${this._id}\">Sibling Container #${this._id}</h2>\n<button id=\"add-receiver\" aria-label=\"Add new receiver component\">Add Receiver</button>\n</div>\n<div class=\"siblings-layout\">\n<div class=\"sibling-column senders\" role=\"group\" aria-label=\"Message senders\">\n<sibling-sender id=\"sender-1\" name=\"A\"></sibling-sender>\n<sibling-sender id=\"sender-2\" name=\"B\"></sibling-sender>\n</div>\n<div class=\"sibling-column receivers\" role=\"group\" aria-label=\"Message receivers\">\n<sibling-receiver id=\"receiver-1\" name=\"X\"></sibling-receiver>\n<sibling-receiver id=\"receiver-2\" name=\"Y\"></sibling-receiver>\n</div>\n</div>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('#add-receiver').addEventListener('click', () => {\nconst receiverColumn = this.shadowRoot.querySelector('.receivers');\nconst newReceiver = document.createElement('sibling-receiver');\n// Generate a unique name for the new receiver\nconst existingReceivers = this.shadowRoot.querySelectorAll('sibling-receiver');\nconst nextLetter = String.fromCharCode(88 + existingReceivers.length); // Start from 'X'\nnewReceiver.setAttribute('name', nextLetter);\nnewReceiver.id = `receiver-${existingReceivers.length + 1}`;\nreceiverColumn.appendChild(newReceiver);\nconsole.log(`🏠 Sibling Container #${this._id}: Added new receiver \"${nextLetter}\"`);\n});\n}\ndisconnectedCallback() {\nconsole.log(`🏠 Sibling Container #${this._id}: Playground closed. Cleaning up...`);\n// Clean up event listeners\nthis.shadowRoot.removeEventListener('message-sent', this.relayMessage);\n}}\nexport class SiblingSender extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\n}\nstatic get observedAttributes() {\nreturn ['name'];\n}\nconnectedCallback() {\nconst name = this.getAttribute('name') || '?';\nconsole.log(`🗣️ Sender ${name} #${this._id}: Ready to send messages!`);\n// Create shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Initial render\nthis.render();\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'name' && this.shadowRoot) {\nthis.render();\n}\n}\nrender() {\nconst name = this.getAttribute('name') || '?';\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 10px 0;\n}\n.sender-card {\nbackground-color: var(--card-bg, white);\nborder-radius: 8px;\nborder: 2px solid #ea4335;\npadding: 12px;\ncolor: var(--text-color, #333);\n}\n.sender-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 8px;\nmargin-bottom: 8px;\n}\nh3 {\nmargin: 0;\ncolor: #ea4335;\n}\n.send-controls {\ndisplay: flex;\nflex-direction: column;\ngap: 8px;\n}\ninput {\npadding: 8px;\nborder: 1px solid var(--border-color, #ddd);\nborder-radius: 4px;\nbackground-color: var(--card-bg, white);\ncolor: var(--text-color, #333);\n}\nbutton {\nbackground-color: #ea4335;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px;\ncursor: pointer;\ntransition: background-color 0.3s;\n}\nbutton:hover {\nbackground-color: #c62828;\n}\nbutton:focus-visible {\noutline: 3px solid #ef9a9a;\noutline-offset: 2px;\n}\n@media (prefers-color-scheme: dark) {\n.sender-card {\nborder-color: #f44336;\n}\nh3 {\ncolor: #f44336;\n}\nbutton:focus-visible {\noutline-color: #e57373;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .sender-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h3 {\ncolor: white;\n}\n:host-context(.high-contrast) input {\nbackground-color: black;\ncolor: white;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button {\nbackground-color: #0080ff;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) .messages-panel {\nbackground-color: #333;\n}\n:host-context(.high-contrast) .message {\nborder: 1px solid #666;\n}\n</style>\n<div class=\"sender-card\" role=\"region\" aria-labelledby=\"sender-heading-${this._id}\">\n<div class=\"sender-header\">\n<h3 id=\"sender-heading-${this._id}\">Sender ${name}</h3>\n<span aria-hidden=\"true\">📣</span>\n</div>\n<div class=\"send-controls\">\n<input type=\"text\" class=\"message-input\"\naria-label=\"Message to send to receivers\"\nplaceholder=\"Type a message\"\nvalue=\"Hello from Sender ${name}!\">\n<button class=\"send-button\" aria-label=\"Send message to all receivers\">Send to All Receivers</button>\n</div>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('.send-button').addEventListener('click', () => {\nconst message = this.shadowRoot.querySelector('.message-input').value;\nconst name = this.getAttribute('name') || '?';\nthis.dispatchEvent(new CustomEvent('message-sent', {\nbubbles: true,\ncomposed: true,\ndetail: {\nmessage,\nsender: name } }));\nconsole.log(`🗣️ Sender ${name} #${this._id}: Sent message \"${message}\"`);\n});\n}\ndisconnectedCallback() {\nconst name = this.getAttribute('name') || '?';\nconsole.log(`🗣️ Sender ${name} #${this._id}: Signing off!`);\n}}\nexport class SiblingReceiver extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._messages = [];\n}\nstatic get observedAttributes() {\nreturn ['name'];\n}\nconnectedCallback() {\nconst name = this.getAttribute('name') || '?';\nconsole.log(`👂 Receiver ${name} #${this._id}: Listening for messages!`);\n// Create shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Initial render\nthis.render();\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'name' && this.shadowRoot) {\nthis.render();\n}\n}\nreceiveMessage(message, sender) {\nconst name = this.getAttribute('name') || '?';\n// Add message to history\nthis._messages.unshift({\ntext: message,\nsender,\ntime: new Date().toLocaleTimeString() });\n// Keep only the 5 most recent messages\nif (this._messages.length > 5) {\nthis._messages.pop();\n}\n// Update the message display\nthis.updateMessages();\nconsole.log(`👂 Receiver ${name} #${this._id}: Received message \"${message}\" from Sender ${sender}`);\n}\nupdateMessages() {\nconst messagesList = this.shadowRoot.querySelector('.messages-list');\nif (!messagesList) return;\nmessagesList.innerHTML = this._messages.length ?\nthis._messages.map(msg => `\n<div class=\"message\">\n<div class=\"message-header\">\n<span class=\"sender\">From ${msg.sender}</span>\n<span class=\"time\">${msg.time}</span>\n</div>\n<div class=\"message-body\">${msg.text}</div>\n</div>\n`).join('') :\n'<div class=\"no-messages\">No messages yet</div>';\n// Animate the newest message\nconst firstMessage = messagesList.querySelector('.message');\nif (firstMessage) {\nfirstMessage.classList.add('new');\nsetTimeout(() => {\nfirstMessage.classList.remove('new');\n}, 1000);\n}\n}\nrender() {\nconst name = this.getAttribute('name') || '?';\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 10px 0;\n}\n.receiver-card {\nbackground-color: var(--card-bg, white);\nborder-radius: 8px;\nborder: 2px solid #9c27b0;\npadding: 12px;\ncolor: var(--text-color, #333);\n}\n.receiver-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 8px;\nmargin-bottom: 8px;\n}\nh3 {\nmargin: 0;\ncolor: #9c27b0;\n}\n.messages-panel {\nbackground-color: var(--highlight-color, #f3e5f5);\nborder-radius: 6px;\npadding: 8px;\nmax-height: 200px;\noverflow-y: auto;\n}\n.messages-list {\ndisplay: flex;\nflex-direction: column;\ngap: 8px;\n}\n.message {\nbackground-color: var(--card-bg, white);\nborder-radius: 6px;\npadding: 8px;\nbox-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n.message.new {\nanimation: highlight-message 1s ease-in-out;\n}\n@keyframes highlight-message {\n0% { background-color: rgba(225, 190, 231, 0.5); }\n100% { background-color: var(--card-bg, white); }\n}\n.message-header {\ndisplay: flex;\njustify-content: space-between;\nfont-size: 0.8em;\ncolor: var(--text-color, #666);\nmargin-bottom: 4px;\nopacity: 0.8;\n}\n.message-body {\nfont-weight: bold;\n}\n.no-messages {\ncolor: var(--text-color, #999);\nopacity: 0.7;\nfont-style: italic;\ntext-align: center;\n}\n@media (prefers-color-scheme: dark) {\n.receiver-card {\nborder-color: #ba68c8;\n}\nh3 {\ncolor: #ba68c8;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .receiver-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h3 {\ncolor: white;\n}\n:host-context(.high-contrast) input {\nbackground-color: black;\ncolor: white;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button {\nbackground-color: #0080ff;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) .messages-panel {\nbackground-color: #333;\n}\n:host-context(.high-contrast) .message {\nborder: 1px solid #666;\n}\n</style>\n<div class=\"receiver-card\" role=\"region\" aria-labelledby=\"receiver-heading-${this._id}\">\n<div class=\"receiver-header\">\n<h3 id=\"receiver-heading-${this._id}\">Receiver ${name}</h3>\n<span aria-hidden=\"true\">👂</span>\n</div>\n<div class=\"messages-panel\">\n<div class=\"messages-list\" role=\"log\" aria-live=\"polite\">\n<div class=\"no-messages\">No messages yet</div>\n</div>\n</div>\n</div>\n`;\n// Update with any existing messages\nthis.updateMessages();\n}\ndisconnectedCallback() {\nconst name = this.getAttribute('name') || '?';\nconsole.log(`👂 Receiver ${name} #${this._id}: Stopping listening!`);\n}}\n// Register the components\ncustomElements.define('sibling-container', SiblingContainer);\ncustomElements.define('sibling-sender', SiblingSender);\ncustomElements.define('sibling-receiver', SiblingReceiver);\nSibling Communication Web Component Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\n3. Global Event Bus: The Town Square\nFor components that are far apart in the DOM tree, a global event bus works like a town square:\n// EventBus.js - The town square where everyone meets\nconst EventBus = new EventTarget();\n// Export it so components can import and use it\nexport { EventBus };\nUsing the event bus in components:\nimport { EventBus } from './EventBus.js';\nclass GlobalSender extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div style=\"border: 1px solid red; padding: 10px;\">\n<h3>Global Announcer</h3>\n<button>Broadcast Announcement</button>\n</div>\n`;\nthis.querySelector('button').addEventListener('click', () => {\nEventBus.dispatchEvent(new CustomEvent('global-announcement', {\ndetail: { message: 'Attention everyone! Important announcement!' }\n}));\n});\n}\n}\nclass GlobalListener extends HTMLElement {\nconnectedCallback() {\nthis.innerHTML = `\n<div style=\"border: 1px solid blue; padding: 10px;\">\n<h3>Global Listener</h3>\n<p>Last announcement: <span class=\"announcement\">None yet</span></p>\n</div>\n`;\n// Subscribe to global events\nEventBus.addEventListener('global-announcement', this.handleAnnouncement.bind(this));\n}\ndisconnectedCallback() {\n// Clean up subscription when element is removed\nEventBus.removeEventListener('global-announcement', this.handleAnnouncement.bind(this));\n}\nhandleAnnouncement(event) {\nthis.querySelector('.announcement').textContent = event.detail.message;\n}\n}\ncustomElements.define('global-sender', GlobalSender);\ncustomElements.define('global-listener', GlobalListener);\nHTML\n<div class=\"event-bus-demo\">\n<global-sender></global-sender>\n<global-listener></global-listener>\n<global-listener></global-listener>\n</div>\nCSS\n/* Base styles */\n:root {\n/* Light mode variables */\n--bg-color: #f5f5f5;\n--text-color: #333;\n--card-bg: #fff;\n--shadow-color: rgba(0,0,0,0.1);\n--border-color: #eee;\n--highlight-color: #f0f0f0;\n/* Button colors */\n--btn-primary: #4CAF50;\n--btn-primary-hover: #45a049;\n--btn-cleanup: #2196F3;\n--btn-cleanup-hover: #0b7dda;\n--btn-mood: #9C27B0;\n--btn-mood-hover: #7B1FA2;\n--btn-remove: #f44336;\n--btn-remove-hover: #d32f2f;\n--btn-adopted: #8bc34a;\n--btn-adopted-hover: #7cb342;\n/* Console colors */\n--console-bg: #333;\n--console-text: #fff;\n--console-birthday: #FF9800;\n--console-cleanup: #2196F3;\n--console-mood: #E91E63;\n--console-adopted: #8bc34a;\n--console-communication: #4CAF50;\n}\n/* Dark mode variables */\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #121212;\n--text-color: #e0e0e0;\n--card-bg: #1e1e1e;\n--shadow-color: rgba(0,0,0,0.3);\n--border-color: #333;\n--highlight-color: #2c2c2c;\n/* Adjust button colors for better contrast in dark mode */\n--btn-primary: #5CDF60;\n--btn-primary-hover: #4cd050;\n--btn-cleanup: #42a6ff;\n--btn-cleanup-hover: #3b95e5;\n--btn-mood: #bb4fd0;\n--btn-mood-hover: #a53fb8;\n--btn-remove: #ff6b60;\n--btn-remove-hover: #e55c52;\n--btn-adopted: #9bd45a;\n--btn-adopted-hover: #8bc34a;\n/* Console colors (keep high contrast) */\n--console-bg: #1a1a1a;\n--console-text: #f0f0f0;\n}\n}\n/* High contrast mode styles for accessibility testing */\n.high-contrast {\n--bg-color: #000;\n--text-color: #fff;\n--card-bg: #222;\n--border-color: #fff;\n--highlight-color: #444;\n/* High contrast button colors */\n--btn-primary: #0080ff;\n--btn-primary-hover: #00a0ff;\n--btn-cleanup: #00c0ff;\n--btn-cleanup-hover: #00e0ff;\n--btn-mood: #ff00ff;\n--btn-mood-hover: #ff40ff;\n--btn-remove: #ff0000;\n--btn-remove-hover: #ff4040;\n--btn-adopted: #00ff00;\n--btn-adopted-hover: #40ff40;\n/* Increase text contrast */\ncolor: #fff;\nbackground-color: #000;\n}\n.high-contrast button {\nborder: 2px solid white;\n}\n.high-contrast h1,\n.high-contrast h2,\n.high-contrast h3 {\ncolor: #fff;\n}\n.high-contrast input,\n.high-contrast select {\nbackground-color: #000;\ncolor: #fff;\nborder: 2px solid #fff;\n}\n/* Debug controls styling */\nbody {\nfont-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\ndisplay: flex;\njustify-content: center;\nalign-items: center;\nmin-height: 100vh;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmargin: 0;\npadding: 20px;\nline-height: 1.5;\n}\n.controls {\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: var(--card-bg);\nborder-radius: 8px;\nbox-shadow: 0 2px 4px var(--shadow-color);\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\n}\nbutton {\nbackground-color: var(--btn-primary);\ncolor: white;\nborder: none;\npadding: 10px 15px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 16px;\ntransition: background-color 0.3s, box-shadow 0.3s;\nposition: relative;\n}\nbutton:focus-visible {\noutline: 3px solid #4d90fe;\noutline-offset: 2px;\n}\nbutton:hover {\nbackground-color: var(--btn-primary-hover);\n}\nbutton.remove {\nbackground-color: var(--btn-remove);\n}\nbutton.remove:hover {\nbackground-color: var(--btn-remove-hover);\n}\n/* Communication tab styles */\n.event-bus-demo {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 20px;\nmargin-top: 20px;\n}\n@media (max-width: 768px) {\n.event-bus-demo {\ngrid-template-columns: 1fr;\n}\n}\nh1, h2, h3, h4, h5, h6 {\ncolor: var(--text-color);\n}\n/* Text contrast utility classes */\n/* Class for elements that need to be monitored for contrast */\nJavaScript\n// Global Communication: Using an EventBus for components that are far apart\nconst EventBus = new EventTarget();\nclass GlobalSender extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._messageCount = 0;\n}\nconnectedCallback() {\nconsole.log(`🌎 Global Sender #${this._id}: Ready to broadcast globally!`);\n// Create shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Initial render\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 10px 0;\n}\n.sender-card {\nbackground-color: var(--card-bg, white);\nborder-radius: 8px;\nborder: 2px solid #673ab7;\npadding: 12px;\ncolor: var(--text-color, #333);\n}\n.sender-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 8px;\nmargin-bottom: 12px;\n}\nh3 {\nmargin: 0;\ncolor: #673ab7;\n}\n.send-controls {\ndisplay: flex;\nflex-direction: column;\ngap: 10px;\n}\n.input-group {\ndisplay: flex;\nflex-direction: column;\ngap: 5px;\n}\nlabel {\nfont-size: 0.9em;\ncolor: var(--text-color, #555);\nopacity: 0.9;\n}\ninput, select {\npadding: 8px;\nborder: 1px solid var(--border-color, #ddd);\nborder-radius: 4px;\nbackground-color: var(--card-bg, white);\ncolor: var(--text-color, #333);\n}\n.buttons {\ndisplay: flex;\ngap: 8px;\nmargin-top: 5px;\n}\nbutton {\nflex: 1;\nbackground-color: #673ab7;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px;\ncursor: pointer;\ntransition: background-color 0.3s;\n}\nbutton:hover {\nbackground-color: #5e35b1;\n}\nbutton:focus-visible {\noutline: 3px solid #b39ddb;\noutline-offset: 2px;\n}\n.announcement {\nbackground-color: #ff5722;\n}\n.announcement:hover {\nbackground-color: #e64a19;\n}\n.message-count {\nbackground-color: var(--highlight-color, #ede7f6);\npadding: 5px 10px;\nborder-radius: 4px;\ntext-align: center;\nfont-size: 0.9em;\nmargin-top: 10px;\n}\n@media (prefers-color-scheme: dark) {\n.sender-card {\nborder-color: #7e57c2;\n}\nh3 {\ncolor: #7e57c2;\n}\nbutton:focus-visible {\noutline-color: #9575cd;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .sender-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h3 {\ncolor: white;\n}\n:host-context(.high-contrast) input,\n:host-context(.high-contrast) select {\nbackground-color: black;\ncolor: white;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) button {\nbackground-color: #0080ff;\nborder: 2px solid white;\n}\n:host-context(.high-contrast) .announcement {\nbackground-color: #ff0000;\n}\n</style>\n<div class=\"sender-card\" role=\"region\" aria-labelledby=\"sender-heading-${this._id}\">\n<div class=\"sender-header\">\n<h3 id=\"sender-heading-${this._id}\">Global Sender #${this._id}</h3>\n<span aria-hidden=\"true\">🌎</span>\n</div>\n<div class=\"send-controls\">\n<div class=\"input-group\">\n<label for=\"message-type-${this._id}\">Message Type:</label>\n<select id=\"message-type-${this._id}\" aria-label=\"Select message type\">\n<option value=\"global-message\">Regular Message</option>\n<option value=\"global-announcement\">Important Announcement</option>\n<option value=\"theme-change\">Theme Change</option>\n</select>\n</div>\n<div class=\"input-group\">\n<label for=\"message-input-${this._id}\">Message:</label>\n<input type=\"text\" id=\"message-input-${this._id}\" value=\"Hello, world!\" aria-label=\"Message content\">\n</div>\n<div class=\"buttons\">\n<button class=\"send-button\" aria-label=\"Send message to listeners\">Send Message</button>\n<button class=\"announcement\" aria-label=\"Send urgent announcement to listeners\">Broadcast Announcement</button>\n</div>\n<div class=\"message-count\" aria-live=\"polite\" aria-atomic=\"true\">\nMessages sent: ${this._messageCount}\n</div>\n</div>\n</div>\n`;\n// Fix the references to the new unique IDs\nthis._messageTypeElem = this.shadowRoot.querySelector(`#message-type-${this._id}`);\nthis._messageInputElem = this.shadowRoot.querySelector(`#message-input-${this._id}`);\n// Add event listeners\nthis.shadowRoot.querySelector('.send-button').addEventListener('click', () => {\nconst messageType = this._messageTypeElem.value;\nconst message = this._messageInputElem.value;\nEventBus.dispatchEvent(new CustomEvent(messageType, {\ndetail: {\nmessage,\nsender: this._id,\ntimestamp: new Date().toLocaleTimeString() } }));\nthis._messageCount++;\nthis.shadowRoot.querySelector('.message-count').textContent = `Messages sent: ${this._messageCount}`;\nconsole.log(`🌎 Global Sender #${this._id}: Dispatched \"${messageType}\" with message \"${message}\"`);\n});\nthis.shadowRoot.querySelector('.announcement').addEventListener('click', () => {\nconst message = this._messageInputElem.value || 'Important announcement!';\nEventBus.dispatchEvent(new CustomEvent('global-announcement', {\ndetail: {\nmessage,\nsender: this._id,\ntimestamp: new Date().toLocaleTimeString(),\nurgent: true } }));\nthis._messageCount++;\nthis.shadowRoot.querySelector('.message-count').textContent = `Messages sent: ${this._messageCount}`;\nconsole.log(`🌎 Global Sender #${this._id}: Dispatched urgent announcement \"${message}\"`);\n});\n}\ndisconnectedCallback() {\nconsole.log(`🌎 Global Sender #${this._id}: Signing off!`);\n}}\nclass GlobalListener extends HTMLElement {\nconstructor() {\nsuper();\nthis._id = Math.floor(Math.random() * 10000);\nthis._messages = [];\n// Bind event handlers\nthis._boundHandlers = {\nmessage: this.handleMessage.bind(this),\nannouncement: this.handleAnnouncement.bind(this),\ntheme: this.handleThemeChange.bind(this) };\n}\nconnectedCallback() {\nconsole.log(`🌍 Global Listener #${this._id}: Listening to global events!`);\n// Create shadow DOM\nthis.attachShadow({ mode: 'open' });\n// Initial render\nthis.render();\n// Subscribe to global events\nEventBus.addEventListener('global-message', this._boundHandlers.message);\nEventBus.addEventListener('global-announcement', this._boundHandlers.announcement);\nEventBus.addEventListener('theme-change', this._boundHandlers.theme);\n}\nhandleMessage(event) {\nconst data = event.detail;\n// Add message to history\nthis._messages.unshift({\ntype: 'message',\n...data });\n// Keep only the 10 most recent messages\nif (this._messages.length > 10) {\nthis._messages.pop();\n}\n// Update the display\nthis.updateMessages();\nconsole.log(`🌍 Global Listener #${this._id}: Received message \"${data.message}\" from Sender #${data.sender}`);\n}\nhandleAnnouncement(event) {\nconst data = event.detail;\n// Add announcement to history\nthis._messages.unshift({\ntype: 'announcement',\n...data });\n// Keep only the 10 most recent messages\nif (this._messages.length > 10) {\nthis._messages.pop();\n}\n// Update the display\nthis.updateMessages();\n// Play an alert sound for urgent announcements\nif (data.urgent) {\nthis.playAlert();\n}\nconsole.log(`🌍 Global Listener #${this._id}: Received ANNOUNCEMENT \"${data.message}\" from Sender #${data.sender}`);\n}\nhandleThemeChange(event) {\nconst data = event.detail;\n// Add theme change to history\nthis._messages.unshift({\ntype: 'theme',\n...data });\n// Keep only the 10 most recent messages\nif (this._messages.length > 10) {\nthis._messages.pop();\n}\n// Update the display\nthis.updateMessages();\n// Change the card theme\nconst card = this.shadowRoot.querySelector('.listener-card');\nif (card) {\ncard.style.backgroundColor = data.message === 'dark' ? '#263238' : '#fff';\n// Also update text color for dark theme\nif (data.message === 'dark') {\ncard.style.color = '#fff';\n} else {\ncard.style.color = '';\n}\n}\nconsole.log(`🌍 Global Listener #${this._id}: Received theme change to \"${data.message}\" from Sender #${data.sender}`);\n}\nplayAlert() {\n// Visual alert\nconst card = this.shadowRoot.querySelector('.listener-card');\nif (card) {\ncard.classList.add('urgent-alert');\nsetTimeout(() => {\ncard.classList.remove('urgent-alert');\n}, 2000);\n}\n}\nupdateMessages() {\nconst messagesList = this.shadowRoot.querySelector('.messages-list');\nif (!messagesList) return;\nmessagesList.innerHTML = this._messages.length ?\nthis._messages.map(msg => `\n<div class=\"message ${msg.type} ${msg.urgent ? 'urgent' : ''}\">\n<div class=\"message-header\">\n<span class=\"type\">${this.getTypeIcon(msg.type)} ${this.getTypeName(msg.type)}</span>\n<span class=\"time\">${msg.timestamp}</span>\n</div>\n<div class=\"message-body\">${msg.message}</div>\n<div class=\"message-footer\">\n<span class=\"sender\">From: Sender #${msg.sender}</span>\n</div>\n</div>\n`).join('') :\n'<div class=\"no-messages\">No messages yet</div>';\n// Animate the newest message\nconst firstMessage = messagesList.querySelector('.message');\nif (firstMessage) {\nfirstMessage.classList.add('new');\nsetTimeout(() => {\nfirstMessage.classList.remove('new');\n}, 1000);\n}\n}\ngetTypeIcon(type) {\nswitch (type) {\ncase 'message':return '💬';\ncase 'announcement':return '📢';\ncase 'theme':return '🎨';\ndefault:return '📩';}\n}\ngetTypeName(type) {\nswitch (type) {\ncase 'message':return 'Message';\ncase 'announcement':return 'Announcement';\ncase 'theme':return 'Theme Change';\ndefault:return 'Unknown';}\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin: 10px 0;\n}\n.listener-card {\nbackground-color: var(--card-bg, white);\nborder-radius: 8px;\nborder: 2px solid #2196f3;\npadding: 12px;\ntransition: all 0.3s ease;\ncolor: var(--text-color, #333);\n}\n.listener-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nborder-bottom: 1px solid var(--border-color, #eee);\npadding-bottom: 8px;\nmargin-bottom: 12px;\n}\nh3 {\nmargin: 0;\ncolor: #2196f3;\n}\n.messages-panel {\nbackground-color: var(--highlight-color, #e3f2fd);\nborder-radius: 6px;\npadding: 8px;\nmax-height: 250px;\noverflow-y: auto;\n}\n.messages-list {\ndisplay: flex;\nflex-direction: column;\ngap: 8px;\n}\n.message {\nbackground-color: var(--card-bg, white);\nborder-radius: 6px;\npadding: 10px;\nbox-shadow: 0 1px 3px rgba(0,0,0,0.1);\n}\n.message.announcement {\nborder-left: 4px solid #ff5722;\n}\n.message.announcement.urgent {\nborder-left: 4px solid #f44336;\nbackground-color: var(--card-bg, #ffebee);\n}\n.message.theme {\nborder-left: 4px solid #9c27b0;\n}\n.message.new {\nanimation: highlight-message 1s ease-in-out;\n}\n@keyframes highlight-message {\n0% { transform: translateX(-10px); opacity: 0; }\n100% { transform: translateX(0); opacity: 1; }\n}\n.message-header {\ndisplay: flex;\njustify-content: space-between;\nfont-size: 0.85em;\ncolor: var(--text-color, #666);\nopacity: 0.8;\nmargin-bottom: 6px;\n}\n.message-body {\nfont-weight: bold;\nmargin-bottom: 6px;\n}\n.message-footer {\nfont-size: 0.8em;\ncolor: var(--text-color, #999);\nopacity: 0.8;\ntext-align: right;\nfont-style: italic;\n}\n.no-messages {\ncolor: var(--text-color, #999);\nopacity: 0.7;\nfont-style: italic;\ntext-align: center;\npadding: 20px;\n}\n.urgent-alert {\nanimation: urgent-pulse 0.5s ease-in-out 4;\n}\n@keyframes urgent-pulse {\n0% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4); }\n70% { box-shadow: 0 0 0 10px rgba(244, 67, 54, 0); }\n100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0); }\n}\n.controls {\ndisplay: flex;\njustify-content: space-between;\nmargin-top: 12px;\n}\n.subscribe-toggle {\nbackground-color: #2196f3;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 12px;\ncursor: pointer;\n}\n.subscribe-toggle:hover {\nbackground-color: #1976d2;\n}\n.subscribe-toggle:focus-visible {\noutline: 3px solid #90caf9;\noutline-offset: 2px;\n}\n.clear-messages {\nbackground-color: #f44336;\ncolor: white;\nborder: none;\nborder-radius: 4px;\npadding: 8px 12px;\ncursor: pointer;\n}\n.clear-messages:hover {\nbackground-color: #d32f2f;\n}\n.clear-messages:focus-visible {\noutline: 3px solid #ef9a9a;\noutline-offset: 2px;\n}\n@media (prefers-color-scheme: dark) {\n.listener-card {\nborder-color: #42a5f5;\n}\nh3 {\ncolor: #42a5f5;\n}\n.message.announcement.urgent {\nbackground-color: rgba(244, 67, 54, 0.2);\n}\n.subscribe-toggle:focus-visible {\noutline-color: #64b5f6;\n}\n.clear-messages:focus-visible {\noutline-color: #e57373;\n}\n}\n/* Support for high contrast mode */\n:host-context(.high-contrast) .listener-card {\nborder: 3px solid white;\nbackground-color: #222;\ncolor: white;\n}\n:host-context(.high-contrast) h3 {\ncolor: white;\n}\n:host-context(.high-contrast) .messages-panel {\nbackground-color: #333;\n}\n:host-context(.high-contrast) .message {\nborder: 1px solid #666;\n}\n:host-context(.high-contrast) .message.urgent {\nborder-left: 5px solid #ff0000;\n}\n</style>\n<div class=\"listener-card\" role=\"region\" aria-labelledby=\"listener-heading-${this._id}\">\n<div class=\"listener-header\">\n<h3 id=\"listener-heading-${this._id}\">Global Listener #${this._id}</h3>\n<span aria-hidden=\"true\">🌍</span>\n</div>\n<div class=\"messages-panel\">\n<div class=\"messages-list\" role=\"log\" aria-live=\"polite\">\n<div class=\"no-messages\">No messages yet - waiting for global broadcasts...</div>\n</div>\n</div>\n<div class=\"controls\">\n<button class=\"subscribe-toggle\" aria-pressed=\"true\">Unsubscribe</button>\n<button class=\"clear-messages\" aria-label=\"Clear message history\">Clear Messages</button>\n</div>\n</div>\n`;\n// Add event listeners\nthis.shadowRoot.querySelector('.subscribe-toggle').addEventListener('click', e => {\nconst button = e.target;\nconst isSubscribed = button.textContent === 'Unsubscribe';\nif (isSubscribed) {\n// Unsubscribe from events\nEventBus.removeEventListener('global-message', this._boundHandlers.message);\nEventBus.removeEventListener('global-announcement', this._boundHandlers.announcement);\nEventBus.removeEventListener('theme-change', this._boundHandlers.theme);\nbutton.textContent = 'Subscribe';\nbutton.style.backgroundColor = '#4caf50';\nthis.shadowRoot.querySelector('.messages-list').innerHTML =\n'<div class=\"no-messages\">Currently unsubscribed from global events</div>';\nconsole.log(`🌍 Global Listener #${this._id}: Unsubscribed from global events`);\n} else {\n// Resubscribe to events\nEventBus.addEventListener('global-message', this._boundHandlers.message);\nEventBus.addEventListener('global-announcement', this._boundHandlers.announcement);\nEventBus.addEventListener('theme-change', this._boundHandlers.theme);\nbutton.textContent = 'Unsubscribe';\nbutton.style.backgroundColor = '#2196f3';\nthis.updateMessages();\nconsole.log(`🌍 Global Listener #${this._id}: Subscribed to global events`);\n}\n});\nthis.shadowRoot.querySelector('.clear-messages').addEventListener('click', () => {\nthis._messages = [];\nthis.shadowRoot.querySelector('.messages-list').innerHTML =\n'<div class=\"no-messages\">No messages yet - waiting for global broadcasts...</div>';\nconsole.log(`🌍 Global Listener #${this._id}: Cleared message history`);\n});\n}\ndisconnectedCallback() {\nconsole.log(`🌍 Global Listener #${this._id}: Disconnecting and cleaning up!`);\n// Unsubscribe from global events\nEventBus.removeEventListener('global-message', this._boundHandlers.message);\nEventBus.removeEventListener('global-announcement', this._boundHandlers.announcement);\nEventBus.removeEventListener('theme-change', this._boundHandlers.theme);\n}}\n// Register the components\ncustomElements.define('global-sender', GlobalSender);\ncustomElements.define('global-listener', GlobalListener);\nGlobal Communication Between Web Components Example on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nPro tip: This pattern works great for notifications, theme changes, or user authentication status that many components need to know about!\nEach action demonstrates a different aspect of Web Components communication!\nCheck out the live demo below, and explore the code to see how it implements the concepts we've learned.\nCommon Pitfalls to Avoid\nForgetting to clean up resources in disconnectedCallback() - This is the #1 cause of memory leaks!\nNot using Shadow DOM when appropriate - Without it, your CSS might leak out or be affected by the parent page\nOverusing attributes for complex data - Consider properties instead for objects and arrays\nIgnoring browser compatibility - Custom elements have worked in every current browser since January 2020 (per MDN), so you only need Can I Use (https://caniuse.com/#feat=custom-elementsv1) and a polyfill if you support older ones\nCreating too many small components - Every component has overhead; find the right balance for your application\nExpert Tips\nDebug attribute changes by adding a console.log in attributeChangedCallback\nCreate a base component class that handles common functionality for all your components\nDocument your components with clear API comments - your future self will thank you!\nUse Shadow DOM for true encapsulation by adding this.attachShadow({mode: 'open'}) in your constructor\nUse custom events for component communication rather than directly accessing other components\nTry It Yourself: Interactive Playground\nHere is a small color-changing component to build on. Click it and it cycles through four colors. Edit the JavaScript tab, press Run, and click the box to check your work. Try the stages in order.\nHTML\n<color-toggler></color-toggler>\nCSS\nbody { font-family: sans-serif; }\nJavaScript\nclass ColorToggler extends HTMLElement {\nconstructor() {\nsuper();\n// TODO (stage 2): turn each color into { hex, name }\nthis.colors = ['#e91e63', '#2196f3', '#4caf50', '#ff9800'];\nthis.currentIndex = 0;\n// Bind once, so removeEventListener later gets the very same function.\nthis.cycleColor = this.cycleColor.bind(this);\n}\nconnectedCallback() {\nthis.style.cssText = 'display:block;padding:20px;margin:20px;color:white;' +\n'border-radius:8px;cursor:pointer;user-select:none;' +\n'transition:background-color 0.3s;background-color:' + this.colors[0];\nthis.innerHTML = '<h3>Click me to change color!</h3>';\nthis.addEventListener('click', this.cycleColor);\n}\ncycleColor() {\nthis.currentIndex = (this.currentIndex + 1) % this.colors.length;\nthis.style.backgroundColor = this.colors[this.currentIndex];\n// TODO (stage 1): update the <h3> text here\n}\ndisconnectedCallback() {\nthis.removeEventListener('click', this.cycleColor);\n}\n}\ncustomElements.define('color-toggler', ColorToggler);\nChallenge: show the color name on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nStage 1. Make the heading say Color 2 of 4 after the first click, then Color 3 of 4, and so on, wrapping back to Color 1 of 4. Expected result: the box changes color and its text changes together.\nStage 2. Give each color a name (Pink, Blue, Green, Orange) by turning the strings into objects, and show the name in the heading instead of the number. Expected result: the first click shows Blue. Careful: once colors holds objects, every place that reads this.colors[...] needs .hex.\nStage 3. Look at the bind line in the constructor. Remove it and use this.addEventListener('click', this.cycleColor.bind(this)) instead. The component still works, but the cleanup in disconnectedCallback quietly stops working: bind makes a brand-new function each time, and removeEventListener only removes the exact function it is given. (In a test, a removed element still counted clicks with the inline bind, and stopped counting with the bound-once version.)\nTip:\nStage 1: this.querySelector('h3').textContent = ... changes the heading you built in connectedCallback, and a template literal such as `Color ${this.currentIndex + 1} of ${this.colors.length}` builds the text. Stage 2: the color is now this.colors[i].hex and the label is this.colors[i].name.\nAdditional Resources:\nMDN Web Components Guide (https://developer.mozilla.org/en-US/docs/Web/Web_Components)\nGitHub Repo for this Demo (https://github.com/mnichols08/WebComponentCallbacks)\nDemo of Web Components Discussed Here (https://mnichols08.github.io/WebComponentCallbacks/)\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-03-23T20:00:53Z",
      "id": "https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/",
      "image": "https://mnixblog.netlify.app/images/lifecycle-callbacks-and-component-communication/cover-v4.jpg",
      "summary": "React when a custom element is added, removed or changed with lifecycle callbacks, then pass messages between components.",
      "tags": [
        "Web Components"
      ],
      "title": "Lifecycle Callbacks & Component Communication",
      "url": "https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/"
    },
    {
      "content_html": "<p>You drop a custom component into a page and the page's CSS turns it into a mess. Or you want to restyle a component from outside and nothing you write seems to reach it. Both come from the same rule: a component's inside is walled off from the page's CSS, and you have to decide where to cut doors in the wall.</p>\n<p>This post shows the four doors that let the page adjust a component on purpose: <code>:host</code>, <code>::slotted</code>, CSS custom properties and <code>::part</code>. The sections after \"Inheritance and Context Adaptation\" (performance, design-system integration, lifecycle) are advanced extras you can skim.</p>\n<p>A two-line recap if you are new here: a custom element is a class that <code>extends HTMLElement</code>, and <code>attachShadow</code> gives it a private shadow tree whose inner elements page CSS cannot select.</p>\n<h2 id=\"shadow-dom-the-technical-foundation-of-style-encapsulation\">Shadow DOM: The Technical Foundation of Style Encapsulation<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#shadow-dom-the-technical-foundation-of-style-encapsulation\" aria-label=\"Link to this section\">#</a></h2>\n<p>The Shadow DOM provides a powerful isolation mechanism that creates a separate DOM tree with its own scoped styling context. This architectural approach solves one of the most persistent challenges in front-end development: CSS collision and specificity conflicts.</p>\n<p>When implementing Shadow DOM encapsulation, you're effectively creating a styling boundary with controlled entry and exit points:</p>\n<pre><code class=\"language-javascript\">class AdvancedComponent extends HTMLElement {\n  constructor() {\n    super();\n    // Create an isolated DOM tree with its own styling context\n    const shadow = this.attachShadow({ \n      mode: 'open',\n      // Optional: focus behavior, not styling. Clicking non-focusable\n      // parts of the shadow tree focuses its first focusable element,\n      // and focusing the host delegates focus to that element.\n      delegatesFocus: true \n    });\n\n    shadow.innerHTML = `\n      &lt;style&gt;\n        :host {\n          display: block;\n          contain: content; /* CSS containment for performance */\n        }\n        .container {\n          background-color: var(--bg-primary, #f0f4f8);\n          padding: var(--spacing-md, 16px);\n          border-radius: var(--border-radius-md, 4px);\n          box-shadow: var(--shadow-sm, 0 1px 3px rgba(0,0,0,0.1));\n          transition: all 180ms ease-out;\n        }\n        ::slotted(*) {\n          margin-bottom: var(--spacing-sm, 8px);\n        }\n      &lt;/style&gt;\n      &lt;div class=\"container\"&gt;\n        &lt;slot&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n  }\n}\n\ncustomElements.define('advanced-component', AdvancedComponent);\n</code></pre>\n<p>This implementation showcases several patterns (note that <code>delegatesFocus</code> has nothing to do with styles; it only changes how focus moves into the component):</p>\n<ul>\n<li>\n<p>The <code>:host</code> selector targets the custom element itself</p>\n</li>\n<li>\n<p>CSS containment optimizes rendering performance</p>\n</li>\n<li>\n<p>CSS custom properties create a styling API</p>\n</li>\n<li>\n<p>The <code>::slotted</code> pseudo-element styles projected content</p>\n</li>\n</ul>\n<p>Seeing is believing when it comes to understanding the true power of Shadow DOM encapsulation. In this interactive example, you can observe how Shadow DOM completely isolates component styles from the surrounding page. Notice how the global styles (with red borders and yellow backgrounds) affect the regular DOM element but have no effect on the Shadow DOM components. Try toggling between light and dark mode to see how components adapt, and inspect the difference between open and closed Shadow DOM modes.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Shadow DOM Style Encapsulation Demo</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h2&gt;Shadow DOM Style Encapsulation&lt;/h2&gt;\n\n&lt;p&gt;\n\tThis demo shows how Shadow DOM protects components from external styles.\n\tThe global styles with red borders and yellow backgrounds have no effect on the Shadow DOM component.\n&lt;/p&gt;\n\n&lt;div class=&quot;demo-container&quot;&gt;\n\t&lt;h3&gt;Regular DOM (No Shadow DOM)&lt;/h3&gt;\n\t&lt;div class=&quot;global-card card&quot;&gt;\n\t\t&lt;h4&gt;Regular DOM Card&lt;/h4&gt;\n\t\t&lt;p&gt;This card uses regular DOM and is affected by global styles.&lt;/p&gt;\n\t&lt;/div&gt;\n\n\t&lt;h3&gt;Shadow DOM (Open Mode)&lt;/h3&gt;\n\t&lt;shadow-card mode=&quot;open&quot;&gt;&lt;/shadow-card&gt;\n\n\t&lt;h3&gt;Shadow DOM (Closed Mode)&lt;/h3&gt;\n\t&lt;shadow-card mode=&quot;closed&quot;&gt;&lt;/shadow-card&gt;\n&lt;/div&gt;\n\n&lt;div class=&quot;button-row&quot;&gt;\n\t&lt;button id=&quot;inspect-open&quot;&gt;Inspect Open Shadow DOM&lt;/button&gt;\n\t&lt;button id=&quot;inspect-closed&quot;&gt;Inspect Closed Shadow DOM&lt;/button&gt;\n\t&lt;button id=&quot;toggle-theme&quot;&gt;Toggle Light/Dark Mode&lt;/button&gt;\n&lt;/div&gt;\n\n&lt;div id=&quot;console-output&quot; style=&quot;border: 1px solid var(--border-color); padding: 10px; border-radius: 4px; margin-top: 20px; background-color: var(--card-bg);&quot;&gt;\n\t&lt;p&gt;Console output will appear here...&lt;/p&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Light/Dark mode variables */\n:root {\n\t--bg-color: #f8f9fa;\n\t--text-color: #333;\n\t--border-color: #dee2e6;\n\t--card-bg: #fff;\n\t--button-primary: #0d6efd;\n\t--button-text: #fff;\n\tcolor-scheme: light dark;\n}\n\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #212529;\n\t\t--text-color: #f8f9fa;\n\t\t--border-color: #495057;\n\t\t--card-bg: #343a40;\n\t\t--button-primary: #0d6efd;\n\t\t--button-text: #fff;\n\t}\n}\n\nbody {\n\tfont-family: system-ui, -apple-system, sans-serif;\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n\ttransition: background-color 0.3s ease;\n}\n\nh2 {\n\tmargin-top: 0;\n}\n\n.demo-container {\n\tmargin: 20px 0;\n}\n\n/* Global styles that won&#39;t affect shadow DOM */\n.global-card {\n\tborder: 2px solid red;\n\tpadding: 20px;\n\tborder-radius: 8px;\n\tbackground-color: yellow;\n\tcolor: purple;\n}\n\np {\n\tcolor: var(--text-color);\n}\n\n.button-row {\n\tdisplay: flex;\n\tgap: 10px;\n\tmargin: 20px 0;\n}\n\nbutton {\n\tpadding: 8px 16px;\n\tbackground-color: var(--button-primary);\n\tcolor: var(--button-text);\n\tborder: none;\n\tborder-radius: 4px;\n\tcursor: pointer;\n}\n\n/* This style tries to target all cards, but won&#39;t affect Shadow DOM */\n.card {\n\tborder: 2px solid red !important;\n\tbackground-color: yellow !important;\n\tcolor: purple !important;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class ShadowCard extends HTMLElement {\n  constructor() {\n    super();\n    const mode = this.getAttribute(&quot;mode&quot;) || &quot;open&quot;;\n    this.shadow = this.attachShadow({ mode });\n\n    this.render();\n  }\n\n  render() {\n    const style = document.createElement(&quot;style&quot;);\n    style.textContent = `\n          :host {\n            display: block;\n            margin: 20px 0;\n          }\n          \n          .card {\n            background-color: var(--card-bg, #fff);\n            border: 1px solid var(--border-color, #dee2e6);\n            border-radius: 8px;\n            padding: 20px;\n            color: var(--text-color, #333);\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n          }\n          \n          h4 {\n            margin-top: 0;\n            color: var(--text-color, #333);\n          }\n        `;\n\n    const card = document.createElement(&quot;div&quot;);\n    card.className = &quot;card&quot;;\n\n    const heading = document.createElement(&quot;h4&quot;);\n    heading.textContent = `Shadow DOM Card (${this.getAttribute(&quot;mode&quot;)} mode)`;\n\n    const paragraph = document.createElement(&quot;p&quot;);\n    paragraph.textContent = `This card uses Shadow DOM in ${this.getAttribute(\n    &quot;mode&quot;)\n    } mode and is protected from global styles.`;\n\n    card.appendChild(heading);\n    card.appendChild(paragraph);\n\n    this.shadow.appendChild(style);\n    this.shadow.appendChild(card);\n  }}\n\n\ncustomElements.define(&quot;shadow-card&quot;, ShadowCard);\n\n// UI functionality\ndocument.getElementById(&quot;inspect-open&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const openCard = document.querySelector(&#39;shadow-card[mode=&quot;open&quot;]&#39;);\n  const log = document.getElementById(&quot;console-output&quot;);\n\n  try {\n    const shadowRoot = openCard.shadowRoot;\n    log.innerHTML = &quot;&lt;p&gt;Open Shadow DOM successfully accessed! ✓&lt;/p&gt;&quot;;\n  } catch (error) {\n    log.innerHTML =\n    &quot;&lt;p&gt;Error accessing Open Shadow DOM: &quot; + error.message + &quot;&lt;/p&gt;&quot;;\n  }\n});\n\ndocument.getElementById(&quot;inspect-closed&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const closedCard = document.querySelector(&#39;shadow-card[mode=&quot;closed&quot;]&#39;);\n  const log = document.getElementById(&quot;console-output&quot;);\n\n  try {\n    const shadowRoot = closedCard.shadowRoot;\n    if (shadowRoot) {\n      log.innerHTML =\n      &quot;&lt;p&gt;Closed Shadow DOM accessed! (This should not happen)&lt;/p&gt;&quot;;\n    } else {\n      log.innerHTML =\n      &quot;&lt;p&gt;Closed Shadow DOM access returned null. ✓ This is expected behavior.&lt;/p&gt;&quot;;\n    }\n  } catch (error) {\n    log.innerHTML =\n    &quot;&lt;p&gt;Error accessing Closed Shadow DOM: &quot; + error.message + &quot;&lt;/p&gt;&quot;;\n  }\n});\n\ndocument.getElementById(&quot;toggle-theme&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const log = document.getElementById(&quot;console-output&quot;);\n\n  // Check current background color to determine current theme\n  const isDark = getComputedStyle(document.body).backgroundColor.match(\n  /^rgb\\(33,\\s*37,\\s*41\\)$/);\n\n\n  if (isDark) {\n    document.documentElement.style.setProperty(&quot;--bg-color&quot;, &quot;#f8f9fa&quot;);\n    document.documentElement.style.setProperty(&quot;--text-color&quot;, &quot;#333&quot;);\n    document.documentElement.style.setProperty(&quot;--border-color&quot;, &quot;#dee2e6&quot;);\n    document.documentElement.style.setProperty(&quot;--card-bg&quot;, &quot;#fff&quot;);\n    log.innerHTML = &quot;&lt;p&gt;Theme changed to light mode&lt;/p&gt;&quot;;\n  } else {\n    document.documentElement.style.setProperty(&quot;--bg-color&quot;, &quot;#212529&quot;);\n    document.documentElement.style.setProperty(&quot;--text-color&quot;, &quot;#f8f9fa&quot;);\n    document.documentElement.style.setProperty(&quot;--border-color&quot;, &quot;#495057&quot;);\n    document.documentElement.style.setProperty(&quot;--card-bg&quot;, &quot;#343a40&quot;);\n    log.innerHTML = &quot;&lt;p&gt;Theme changed to dark mode&lt;/p&gt;&quot;;\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<h3 id=\"understanding-shadow-dom-modes-open-vs-closed\">Understanding Shadow DOM Modes: Open vs. Closed<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#understanding-shadow-dom-modes-open-vs-closed\" aria-label=\"Link to this section\">#</a></h3>\n<p>When attaching a shadow root to a component, the <code>mode</code> property defines a critical aspect of your component's encapsulation model. This choice directly impacts both styling and JavaScript interaction capabilities:</p>\n<pre><code class=\"language-javascript\">// Open mode: External JavaScript can access the shadow DOM\nconst openShadow = element.attachShadow({ mode: 'open' });\n\n// Closed mode: External JavaScript cannot directly access the shadow DOM\nconst closedShadow = element.attachShadow({ mode: 'closed' });\n</code></pre>\n<h4 id=\"when-to-use-mode-closed\">When to Use <code>mode: 'closed'</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#when-to-use-mode-closed\" aria-label=\"Link to this section\">#</a></h4>\n<p><em>Recap of the Shadow DOM post:</em> <code>open</code> lets page scripts reach the shadow tree through <code>element.shadowRoot</code>; <code>closed</code> makes that property return <code>null</code>. It matters little for styling, so feel free to skip ahead to \"Advanced Styling Architectures\".</p>\n<p>The <code>closed</code> mode creates a stricter boundary around your component, preventing external JavaScript from accessing its shadow DOM through the <code>.shadowRoot</code> property. While <code>open</code> mode is more common for most use cases, <code>closed</code> mode serves specific technical purposes:</p>\n<ol>\n<li>\n<p><strong>Security-Sensitive Components</strong>: When building components that process sensitive data (payment forms, authentication interfaces)</p>\n</li>\n<li>\n<p><strong>Preventing DOM Manipulation</strong>: To stop external scripts from modifying your component's internal structure</p>\n</li>\n<li>\n<p><strong>Strict API Enforcement</strong>: Ensuring that all interactions happen through your explicitly defined APIs</p>\n</li>\n<li>\n<p><strong>Third-Party Protection</strong>: Protecting your component from interference by third-party scripts on the page</p>\n</li>\n<li>\n<p><strong>Browser-Like Components</strong>: When emulating native browser elements that use closed shadow roots</p>\n</li>\n</ol>\n<p>One correction before the example: <code>closed</code> is a guard rail, not a security boundary. Other scripts on the page can still change the DOM around your component, and a script that runs before yours can patch <code>attachShadow</code> to keep a reference to the root. Do not rely on it to protect sensitive data.</p>\n<p>Here's an implementation example of a component with a closed shadow root:</p>\n<pre><code class=\"language-javascript\">class SecureComponent extends HTMLElement {\n  constructor() {\n    super();\n\n    // Create a closed shadow root\n    const shadow = this.attachShadow({ mode: 'closed' });\n\n    // Store internal state and references privately using closure\n    const state = {\n      value: '',\n      isValid: false\n    };\n\n    // Create internal DOM structure\n    const input = document.createElement('input');\n    input.type = 'password';\n    input.setAttribute('placeholder', 'Enter secure data');\n\n    const statusIndicator = document.createElement('div');\n    statusIndicator.className = 'status-indicator';\n\n    // Add styles with maximum encapsulation\n    const style = document.createElement('style');\n    style.textContent = `\n      :host {\n        display: block;\n        border: 1px solid #ddd;\n        border-radius: 4px;\n        padding: 16px;\n        background: #f9f9f9;\n      }\n\n      input {\n        border: 1px solid #ccc;\n        padding: 8px;\n        border-radius: 4px;\n        width: 100%;\n        box-sizing: border-box;\n      }\n\n      .status-indicator {\n        height: 8px;\n        margin-top: 8px;\n        border-radius: 4px;\n        background-color: #ccc;\n        transition: background-color 0.3s ease;\n      }\n\n      .status-indicator.valid {\n        background-color: #4caf50;\n      }\n\n      .status-indicator.invalid {\n        background-color: #f44336;\n      }\n    `;\n\n    // Add event listeners that maintain encapsulation\n    input.addEventListener('input', (e) =&gt; {\n      state.value = e.target.value;\n      this._validateAndUpdate(state, statusIndicator);\n\n      // Communicate changes through events rather than direct access\n      this.dispatchEvent(new CustomEvent('secure-input-change', {\n        bubbles: true,\n        composed: true,\n        detail: { isValid: state.isValid }\n      }));\n    });\n\n    // Add elements to shadow DOM\n    shadow.appendChild(style);\n    shadow.appendChild(input);\n    shadow.appendChild(statusIndicator);\n\n    // Expose methods through the element itself, not through shadowRoot\n    this.reset = () =&gt; {\n      state.value = '';\n      state.isValid = false;\n      input.value = '';\n      statusIndicator.className = 'status-indicator';\n    };\n\n    this.getValue = () =&gt; state.isValid ? state.value : null;\n  }\n\n  // Private method (uses closure to access internal elements)\n  _validateAndUpdate(state, indicator) {\n    // Simple validation logic\n    state.isValid = state.value.length &gt;= 8;\n\n    // Update UI to reflect state\n    indicator.className = state.isValid \n      ? 'status-indicator valid' \n      : 'status-indicator invalid';\n  }\n\n  // Lifecycle methods still work normally\n  connectedCallback() {\n    console.log('Secure component connected');\n  }\n\n  disconnectedCallback() {\n    console.log('Secure component disconnected');\n  }\n}\n\ncustomElements.define('secure-component', SecureComponent);\n</code></pre>\n<p>This secure component demonstrates key principles of closed shadow DOM usage:</p>\n<ol>\n<li>\n<p><strong>Private State Management</strong>: State is maintained in closure, inaccessible to external scripts</p>\n</li>\n<li>\n<p><strong>Explicit API Design</strong>: Functionality is exposed through carefully designed methods on the element</p>\n</li>\n<li>\n<p><strong>Event-Based Communication</strong>: Changes are announced through custom events (the component calls <code>this.dispatchEvent(new CustomEvent('name', { detail }))</code> and the page listens with <code>addEventListener</code>)</p>\n</li>\n<li>\n<p><strong>Style Encapsulation</strong>: Styles are fully contained and inaccessible externally</p>\n</li>\n</ol>\n<h4 id=\"technical-considerations-and-tradeoffs\">Technical Considerations and Tradeoffs<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#technical-considerations-and-tradeoffs\" aria-label=\"Link to this section\">#</a></h4>\n<p>While <code>mode: 'closed'</code> offers stronger encapsulation, it comes with significant tradeoffs:</p>\n<ol>\n<li>\n<p><strong>Debugging Challenges</strong>: Inspecting and debugging closed shadow DOMs is more difficult</p>\n</li>\n<li>\n<p><strong>Testing Complexity</strong>: Unit tests can't easily access internal elements</p>\n</li>\n<li>\n<p><strong>External Styling Limitations</strong>: Makes providing styling hooks slightly more complex</p>\n</li>\n<li>\n<p><strong>Third-Party Extensions</strong>: Prevents legitimate extensions or enhancements</p>\n</li>\n<li>\n<p><strong>Developer Experience</strong>: Can create friction for legitimate component users</p>\n</li>\n</ol>\n<p>Most component libraries choose <code>mode: 'open'</code> for these reasons, as it provides sufficient encapsulation while maintaining better developer experience. Reserve <code>mode: 'closed'</code> for cases where the enhanced security and encapsulation are explicitly required by the component's purpose.</p>\n<p><strong>Note on Lifecycle Implications</strong>: While shadow DOM mode doesn't directly affect component lifecycle methods like <code>connectedCallback</code> or <code>disconnectedCallback</code>, it does influence how those methods interact with the component's internal DOM. With closed mode, lifecycle methods need to rely on closure variables or instance properties to manipulate the shadow DOM, as <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the lifecycle post</a> explains.</p>\n<h2 id=\"advanced-styling-architectures\">Advanced Styling Architectures<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#advanced-styling-architectures\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"1-dynamic-style-injection-with-performance-optimization-advanced-optional\">1. Dynamic Style Injection with Performance Optimization (advanced, optional)<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#1-dynamic-style-injection-with-performance-optimization-advanced-optional\" aria-label=\"Link to this section\">#</a></h3>\n<p>For components requiring dynamic styling based on state or props, a more sophisticated approach involves programmatically generating and injecting styles with performance considerations:</p>\n<pre><code class=\"language-javascript\">class DynamicStyledComponent extends HTMLElement {\n  static get observedAttributes() {\n    return ['variant', 'size', 'disabled'];\n  }\n\n  constructor() {\n    super();\n    this.shadow = this.attachShadow({ mode: 'open' });\n    this._styleElement = document.createElement('style');\n    this.shadow.appendChild(this._styleElement);\n\n    // Create the component structure\n    const container = document.createElement('div');\n    container.className = 'container';\n    container.innerHTML = '&lt;slot&gt;&lt;/slot&gt;';\n    this.shadow.appendChild(container);\n\n    // Initial render\n    this._updateStyles();\n  }\n\n  attributeChangedCallback() {\n    // Debounced style updates for performance\n    if (this._styleUpdateScheduled) return;\n    this._styleUpdateScheduled = true;\n\n    requestAnimationFrame(() =&gt; {\n      this._updateStyles();\n      this._styleUpdateScheduled = false;\n    });\n  }\n\n  _updateStyles() {\n    const variant = this.getAttribute('variant') || 'default';\n    const size = this.getAttribute('size') || 'medium';\n    const disabled = this.hasAttribute('disabled');\n\n    const variantStyles = {\n      default: {\n        backgroundColor: 'var(--color-surface, white)',\n        color: 'var(--color-text, #333)'\n      },\n      primary: {\n        backgroundColor: 'var(--color-primary, #0070f3)',\n        color: 'var(--color-primary-contrast, white)'\n      },\n      // Additional variants\n    };\n\n    const sizeStyles = {\n      small: { padding: '8px 12px', fontSize: '14px' },\n      medium: { padding: '12px 16px', fontSize: '16px' },\n      large: { padding: '16px 24px', fontSize: '18px' }\n    };\n\n    const selectedVariant = variantStyles[variant] || variantStyles.default;\n    const selectedSize = sizeStyles[size] || sizeStyles.medium;\n\n    // Generate optimized CSS\n    this._styleElement.textContent = `\n      .container {\n        ${Object.entries(selectedVariant).map(([prop, value]) =&gt; `${prop}: ${value};`).join('\\n        ')}\n        ${Object.entries(selectedSize).map(([prop, value]) =&gt; `${prop}: ${prop}: ${value};`).join('\\n        ')}\n        ${disabled ? 'opacity: 0.6; pointer-events: none;' : ''}\n        border-radius: var(--border-radius, 4px);\n        transition: box-shadow 150ms ease-in-out;\n      }\n\n      .container:hover {\n        ${disabled ? '' : 'box-shadow: var(--shadow-hover, 0 4px 8px rgba(0,0,0,0.1));'}\n      }\n    `;\n  }\n}\n\ncustomElements.define('dynamic-styled-component', DynamicStyledComponent);\n</code></pre>\n<p>This implementation demonstrates several advanced techniques:</p>\n<ul>\n<li>\n<p>Attribute-driven styling with observed attributes</p>\n</li>\n<li>\n<p>Performance optimization via RAF debouncing</p>\n</li>\n<li>\n<p>Dynamic style generation based on component state</p>\n</li>\n<li>\n<p>Conditional style application</p>\n</li>\n</ul>\n<p>This demonstration brings to life the dynamic styling patterns we've just explored. Experiment with different variants, sizes, and states to see how attribute-driven styling transforms the component in real-time. The implementation uses requestAnimationFrame for performance optimization and demonstrates how CSS custom properties create a flexible styling API while maintaining visual consistency. Try toggling between light and dark modes to see how the component adapts to different color schemes.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Dynamic Styling Component</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Dynamic Web Component Styling&lt;/h1&gt;\n\n&lt;p&gt;This demonstration shows a dynamic web component whose appearance updates in real-time based on attributes.&lt;/p&gt;\n\n&lt;div class=&quot;configuration-panel&quot;&gt;\n\t&lt;h3&gt;Component Configuration&lt;/h3&gt;\n\n\t&lt;div class=&quot;control-row&quot;&gt;\n\t\t&lt;label&gt;\n\t\t\tVariant:\n\t\t\t&lt;select id=&quot;variant-select&quot;&gt;\n\t\t\t\t&lt;option value=&quot;primary&quot;&gt;Primary&lt;/option&gt;\n\t\t\t\t&lt;option value=&quot;success&quot;&gt;Success&lt;/option&gt;\n\t\t\t\t&lt;option value=&quot;warning&quot;&gt;Warning&lt;/option&gt;\n\t\t\t\t&lt;option value=&quot;danger&quot;&gt;Danger&lt;/option&gt;\n\t\t\t\t&lt;option value=&quot;info&quot;&gt;Info&lt;/option&gt;\n\t\t\t&lt;/select&gt;\n\t\t&lt;/label&gt;\n\n\t\t&lt;label&gt;\n\t\t\tSize:\n\t\t\t&lt;select id=&quot;size-select&quot;&gt;\n\t\t\t\t&lt;option value=&quot;small&quot;&gt;Small&lt;/option&gt;\n\t\t\t\t&lt;option value=&quot;medium&quot; selected&gt;Medium&lt;/option&gt;\n\t\t\t\t&lt;option value=&quot;large&quot;&gt;Large&lt;/option&gt;\n\t\t\t&lt;/select&gt;\n\t\t&lt;/label&gt;\n\n\t\t&lt;label&gt;\n\t\t\t&lt;input type=&quot;checkbox&quot; id=&quot;disabled-checkbox&quot;&gt;\n\t\t\tDisabled\n\t\t&lt;/label&gt;\n\n\t\t&lt;label&gt;\n\t\t\t&lt;input type=&quot;checkbox&quot; id=&quot;rounded-checkbox&quot; checked&gt;\n\t\t\tRounded\n\t\t&lt;/label&gt;\n\n\t\t&lt;label&gt;\n\t\t\t&lt;input type=&quot;checkbox&quot; id=&quot;outlined-checkbox&quot;&gt;\n\t\t\tOutlined\n\t\t&lt;/label&gt;\n\t&lt;/div&gt;\n\n\t&lt;div class=&quot;control-row&quot;&gt;\n\t\t&lt;label&gt;\n\t\t\tContent:\n\t\t\t&lt;input type=&quot;text&quot; id=&quot;content-input&quot; value=&quot;Dynamic Component&quot; style=&quot;width: 200px;&quot;&gt;\n\t\t&lt;/label&gt;\n\n\t\t&lt;button id=&quot;update-content&quot;&gt;Update Content&lt;/button&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n\n&lt;div class=&quot;demo-container&quot;&gt;\n\t&lt;h3&gt;Demo&lt;/h3&gt;\n\t&lt;dynamic-card variant=&quot;primary&quot; size=&quot;medium&quot; rounded&gt;\n\t\tDynamic Component\n\t&lt;/dynamic-card&gt;\n&lt;/div&gt;\n\n&lt;div class=&quot;toggle-wrapper&quot;&gt;\n\t&lt;button id=&quot;toggle-theme&quot;&gt;Toggle Light/Dark Mode&lt;/button&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">:root {\n\t/* Light theme variables */\n\t--bg-color: #f8f9fa;\n\t--text-color: #212529;\n\t--border-color: #dee2e6;\n\t--card-bg: #fff;\n\t--control-bg: #e9ecef;\n\t--control-border: #ced4da;\n\t--primary-color: #0d6efd;\n\t--success-color: #198754;\n\t--warning-color: #ffc107;\n\t--danger-color: #dc3545;\n\t--info-color: #0dcaf0;\n\tcolor-scheme: light dark;\n}\n\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t--bg-color: #212529;\n\t\t--text-color: #f8f9fa;\n\t\t--border-color: #495057;\n\t\t--card-bg: #343a40;\n\t\t--control-bg: #495057;\n\t\t--control-border: #6c757d;\n\t\t--primary-color: #0d6efd;\n\t\t--success-color: #198754;\n\t\t--warning-color: #ffc107;\n\t\t--danger-color: #dc3545;\n\t\t--info-color: #0dcaf0;\n\t}\n}\n\nbody {\n\tfont-family: system-ui, -apple-system, sans-serif;\n\tbackground-color: var(--bg-color);\n\tcolor: var(--text-color);\n\tmax-width: 800px;\n\tmargin: 0 auto;\n\tpadding: 20px;\n\ttransition: background-color 0.3s ease;\n\tline-height: 1.5;\n}\n\n.configuration-panel {\n\tmargin-bottom: 20px;\n\tpadding: 20px;\n\tborder-radius: 8px;\n\tbackground-color: var(--card-bg);\n\tborder: 1px solid var(--border-color);\n}\n\n.control-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n\tmargin-bottom: 10px;\n}\n\nbutton {\n\tpadding: 8px 16px;\n\tbackground-color: var(--primary-color);\n\tcolor: white;\n\tborder: none;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\ttransition: opacity 0.2s;\n}\n\nbutton:hover {\n\topacity: 0.9;\n}\n\nselect,\ninput {\n\tpadding: 8px;\n\tborder: 1px solid var(--control-border);\n\tborder-radius: 4px;\n\tbackground-color: var(--control-bg);\n\tcolor: var(--text-color);\n}\n\nlabel {\n\tmargin-right: 8px;\n\tdisplay: flex;\n\talign-items: center;\n}\n\nh1,\nh2,\nh3 {\n\tcolor: var(--text-color);\n}\n\n.demo-container {\n\tpadding: 20px;\n\tborder-radius: 8px;\n\tbackground-color: var(--card-bg);\n\tborder: 1px solid var(--border-color);\n}\n\n.toggle-wrapper {\n\tmargin-top: 20px;\n\tdisplay: flex;\n\tjustify-content: flex-end;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class DynamicCard extends HTMLElement {\n  static get observedAttributes() {\n    return [&quot;variant&quot;, &quot;size&quot;, &quot;disabled&quot;, &quot;rounded&quot;, &quot;outlined&quot;];\n  }\n\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this._styleElement = document.createElement(&quot;style&quot;);\n    this.shadowRoot.appendChild(this._styleElement);\n\n    this._container = document.createElement(&quot;div&quot;);\n    this._container.className = &quot;dynamic-card&quot;;\n    this._container.innerHTML = &quot;&lt;slot&gt;&lt;/slot&gt;&quot;;\n    this.shadowRoot.appendChild(this._container);\n\n    this._updateStyles();\n  }\n\n  attributeChangedCallback() {\n    // Debounced style updates for performance\n    if (this._styleUpdateScheduled) return;\n    this._styleUpdateScheduled = true;\n\n    requestAnimationFrame(() =&gt; {\n      this._updateStyles();\n      this._styleUpdateScheduled = false;\n    });\n  }\n\n  _updateStyles() {\n    const variant = this.getAttribute(&quot;variant&quot;) || &quot;primary&quot;;\n    const size = this.getAttribute(&quot;size&quot;) || &quot;medium&quot;;\n    const disabled = this.hasAttribute(&quot;disabled&quot;);\n    const rounded = this.hasAttribute(&quot;rounded&quot;);\n    const outlined = this.hasAttribute(&quot;outlined&quot;);\n\n    const variantStyles = {\n      primary: {\n        backgroundColor: outlined ? &quot;transparent&quot; : &quot;var(--primary-color, #0d6efd)&quot;,\n        color: outlined ? &quot;var(--primary-color, #0d6efd)&quot; : &quot;white&quot;,\n        borderColor: &quot;var(--primary-color, #0d6efd)&quot; },\n\n      success: {\n        backgroundColor: outlined ? &quot;transparent&quot; : &quot;var(--success-color, #198754)&quot;,\n        color: outlined ? &quot;var(--success-color, #198754)&quot; : &quot;white&quot;,\n        borderColor: &quot;var(--success-color, #198754)&quot; },\n\n      warning: {\n        backgroundColor: outlined ? &quot;transparent&quot; : &quot;var(--warning-color, #ffc107)&quot;,\n        color: outlined ? &quot;var(--warning-color, #ffc107)&quot; : &quot;black&quot;,\n        borderColor: &quot;var(--warning-color, #ffc107)&quot; },\n\n      danger: {\n        backgroundColor: outlined ? &quot;transparent&quot; : &quot;var(--danger-color, #dc3545)&quot;,\n        color: outlined ? &quot;var(--danger-color, #dc3545)&quot; : &quot;white&quot;,\n        borderColor: &quot;var(--danger-color, #dc3545)&quot; },\n\n      info: {\n        backgroundColor: outlined ? &quot;transparent&quot; : &quot;var(--info-color, #0dcaf0)&quot;,\n        color: outlined ? &quot;var(--info-color, #0dcaf0)&quot; : &quot;black&quot;,\n        borderColor: &quot;var(--info-color, #0dcaf0)&quot; } };\n\n\n\n    const sizeStyles = {\n      small: {\n        padding: &quot;8px 12px&quot;,\n        fontSize: &quot;14px&quot; },\n\n      medium: {\n        padding: &quot;12px 20px&quot;,\n        fontSize: &quot;16px&quot; },\n\n      large: {\n        padding: &quot;16px 28px&quot;,\n        fontSize: &quot;18px&quot; } };\n\n\n\n    const selectedVariant = variantStyles[variant] || variantStyles.primary;\n    const selectedSize = sizeStyles[size] || sizeStyles.medium;\n\n    // Generate optimized CSS\n    this._styleElement.textContent = `\n          .dynamic-card {\n            display: inline-block;\n            background-color: ${selectedVariant.backgroundColor};\n            color: ${selectedVariant.color};\n            border: 2px solid ${selectedVariant.borderColor};\n            ${Object.entries(selectedSize).\n    map(([prop, value]) =&gt; `${prop}: ${value};`).\n    join(&quot;\\n            &quot;)}\n            border-radius: ${rounded ? &quot;50px&quot; : &quot;4px&quot;};\n            transition: all 0.2s ease-in-out;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n            ${\n    disabled ?\n    &quot;opacity: 0.6; cursor: not-allowed; pointer-events: none;&quot; :\n    &quot;cursor: pointer;&quot;\n    }\n          }\n          \n          .dynamic-card:hover {\n            ${\n    disabled ?\n    &quot;&quot; :\n    &quot;transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15);&quot;\n    }\n          }\n        `;\n  }}\n\n\ncustomElements.define(&quot;dynamic-card&quot;, DynamicCard);\n\n// UI control functionality\nconst dynamicCard = document.querySelector(&quot;dynamic-card&quot;);\n\ndocument.getElementById(&quot;variant-select&quot;).addEventListener(&quot;change&quot;, e =&gt; {\n  dynamicCard.setAttribute(&quot;variant&quot;, e.target.value);\n});\n\ndocument.getElementById(&quot;size-select&quot;).addEventListener(&quot;change&quot;, e =&gt; {\n  dynamicCard.setAttribute(&quot;size&quot;, e.target.value);\n});\n\ndocument.getElementById(&quot;disabled-checkbox&quot;).addEventListener(&quot;change&quot;, e =&gt; {\n  if (e.target.checked) {\n    dynamicCard.setAttribute(&quot;disabled&quot;, &quot;&quot;);\n  } else {\n    dynamicCard.removeAttribute(&quot;disabled&quot;);\n  }\n});\n\ndocument.getElementById(&quot;rounded-checkbox&quot;).addEventListener(&quot;change&quot;, e =&gt; {\n  if (e.target.checked) {\n    dynamicCard.setAttribute(&quot;rounded&quot;, &quot;&quot;);\n  } else {\n    dynamicCard.removeAttribute(&quot;rounded&quot;);\n  }\n});\n\ndocument.getElementById(&quot;outlined-checkbox&quot;).addEventListener(&quot;change&quot;, e =&gt; {\n  if (e.target.checked) {\n    dynamicCard.setAttribute(&quot;outlined&quot;, &quot;&quot;);\n  } else {\n    dynamicCard.removeAttribute(&quot;outlined&quot;);\n  }\n});\n\ndocument.getElementById(&quot;update-content&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const content = document.getElementById(&quot;content-input&quot;).value;\n  dynamicCard.textContent = content;\n});\n\ndocument.getElementById(&quot;toggle-theme&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  // Check current background color to determine current theme\n  const isDark = getComputedStyle(document.body).backgroundColor.match(\n  /rgb\\(33,\\s*37,\\s*41\\)/);\n\n\n  if (isDark) {\n    document.documentElement.style.setProperty(&quot;--bg-color&quot;, &quot;#f8f9fa&quot;);\n    document.documentElement.style.setProperty(&quot;--text-color&quot;, &quot;#212529&quot;);\n    document.documentElement.style.setProperty(&quot;--border-color&quot;, &quot;#dee2e6&quot;);\n    document.documentElement.style.setProperty(&quot;--card-bg&quot;, &quot;#fff&quot;);\n    document.documentElement.style.setProperty(&quot;--control-bg&quot;, &quot;#e9ecef&quot;);\n    document.documentElement.style.setProperty(&quot;--control-border&quot;, &quot;#ced4da&quot;);\n  } else {\n    document.documentElement.style.setProperty(&quot;--bg-color&quot;, &quot;#212529&quot;);\n    document.documentElement.style.setProperty(&quot;--text-color&quot;, &quot;#f8f9fa&quot;);\n    document.documentElement.style.setProperty(&quot;--border-color&quot;, &quot;#495057&quot;);\n    document.documentElement.style.setProperty(&quot;--card-bg&quot;, &quot;#343a40&quot;);\n    document.documentElement.style.setProperty(&quot;--control-bg&quot;, &quot;#495057&quot;);\n    document.documentElement.style.setProperty(&quot;--control-border&quot;, &quot;#6c757d&quot;);\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<h3 id=\"2-constructable-stylesheets-for-runtime-optimization-advanced-optional\">2. Constructable Stylesheets for Runtime Optimization (advanced, optional)<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#2-constructable-stylesheets-for-runtime-optimization-advanced-optional\" aria-label=\"Link to this section\">#</a></h3>\n<p>So far each component has carried its own <code>&lt;style&gt;</code> tag, so a hundred cards means a hundred copies of the same CSS. A constructable stylesheet is a stylesheet you create in JavaScript once and share. <code>new CSSStyleSheet()</code> makes an empty one, <code>replaceSync(cssText)</code> fills it with CSS immediately (no network, no waiting), and <code>shadow.adoptedStyleSheets = [sheet]</code> lets a shadow root use it. Many components can adopt the same sheet object. This is an optimization, not something you need on day one:</p>\n<pre><code class=\"language-javascript\">// Create module-scoped stylesheets\nconst baseStyles = new CSSStyleSheet();\nbaseStyles.replaceSync(`\n  :host {\n    display: block;\n    font-family: var(--font-family, system-ui);\n  }\n  .base {\n    box-sizing: border-box;\n    width: 100%;\n  }\n`);\n\nconst themeStyles = new CSSStyleSheet();\nthemeStyles.replaceSync(`\n  .themed {\n    color: var(--text-color, #212121);\n    background: var(--background-color, #ffffff);\n  }\n`);\n\n// Component implementation with shared stylesheets\nclass OptimizedComponent extends HTMLElement {\n  constructor() {\n    super();\n    const shadow = this.attachShadow({ mode: 'open' });\n\n    // Adopt shared stylesheets (no duplication in memory)\n    shadow.adoptedStyleSheets = [baseStyles, themeStyles];\n\n    // Create component structure\n    shadow.innerHTML = `\n      &lt;div class=\"base themed\"&gt;\n        &lt;slot&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n  }\n}\n\ncustomElements.define('optimized-component', OptimizedComponent);\n</code></pre>\n<p>The technical advantages of this approach include:</p>\n<ul>\n<li>\n<p>Sheet objects are parsed once but used by multiple component instances</p>\n</li>\n<li>\n<p>Memory usage is significantly reduced compared to duplicated inline styles</p>\n</li>\n<li>\n<p>Browser rendering optimizations can be applied to shared stylesheets</p>\n</li>\n<li>\n<p>Updates to shared stylesheets automatically propagate to all components</p>\n</li>\n</ul>\n<h2 id=\"creating-a-component-design-system-with-css-custom-properties\">Creating a Component Design System with CSS Custom Properties<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#creating-a-component-design-system-with-css-custom-properties\" aria-label=\"Link to this section\">#</a></h2>\n<p>To implement a cohesive design system through Web Components, a systematic approach to CSS custom properties creates a powerful styling API:</p>\n<pre><code class=\"language-javascript\">// design-system.js\nconst designSystem = {\n  // Define all design tokens at the :root level\n  installGlobalTokens() {\n    const tokens = document.createElement('style');\n    tokens.textContent = `\n      :root {\n        /* Color system */\n        --color-primary-50: #e3f2fd;\n        --color-primary-100: #bbdefb;\n        --color-primary-500: #2196f3;\n        --color-primary-900: #0d47a1;\n\n        /* Typography */\n        --font-size-xs: 12px;\n        --font-size-sm: 14px;\n        --font-size-md: 16px;\n        --font-size-lg: 18px;\n        --font-size-xl: 20px;\n\n        /* Spacing system */\n        --spacing-unit: 4px;\n        --spacing-xs: calc(var(--spacing-unit) * 1);\n        --spacing-sm: calc(var(--spacing-unit) * 2);\n        --spacing-md: calc(var(--spacing-unit) * 4);\n        --spacing-lg: calc(var(--spacing-unit) * 6);\n        --spacing-xl: calc(var(--spacing-unit) * 10);\n\n        /* Elevation */\n        --shadow-1: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);\n        --shadow-2: 0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12);\n        --shadow-3: 0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10);\n\n        /* Border radius */\n        --radius-sm: 2px;\n        --radius-md: 4px;\n        --radius-lg: 8px;\n        --radius-full: 999px;\n\n        /* Animation */\n        --duration-fast: 150ms;\n        --duration-normal: 300ms;\n        --duration-slow: 500ms;\n        --easing-standard: cubic-bezier(0.4, 0.0, 0.2, 1);\n      }\n    `;\n    document.head.appendChild(tokens);\n  },\n\n  // Component tokens derived from global tokens\n  getComponentTokens(name) {\n    const style = document.createElement('style');\n\n    // Component-specific custom properties derived from global tokens\n    const componentTokens = {\n      'ds-button': `\n        --button-bg: var(--color-primary-500);\n        --button-color: white;\n        --button-padding: var(--spacing-sm) var(--spacing-md);\n        --button-radius: var(--radius-md);\n        --button-shadow: var(--shadow-1);\n      `,\n      'ds-card': `\n        --card-bg: white;\n        --card-border: 1px solid rgba(0,0,0,0.1);\n        --card-padding: var(--spacing-md);\n        --card-radius: var(--radius-md);\n        --card-shadow: var(--shadow-1);\n      `\n    };\n\n    style.textContent = `\n      ${name} {\n        ${componentTokens[name] || ''}\n      }\n    `;\n\n    return style;\n  }\n};\n\n// Install design system tokens\ndocument.addEventListener('DOMContentLoaded', () =&gt; {\n  designSystem.installGlobalTokens();\n});\n</code></pre>\n<p>This approach enables several advanced styling capabilities:</p>\n<ul>\n<li>\n<p>Centralized design token management</p>\n</li>\n<li>\n<p>Hierarchical theming with cascading defaults</p>\n</li>\n<li>\n<p>Component-specific styling APIs that respect the global design system</p>\n</li>\n<li>\n<p>Runtime theming capabilities without component modification</p>\n</li>\n</ul>\n<p>Try a design system built with CSS custom properties. This interactive example showcases how design tokens cascade through your component hierarchy, creating a consistent visual language. The design system automatically adapts between light and dark modes while maintaining component-specific styling APIs. Explore the color tokens, typography scale, and spacing system that form the foundation of the design system, then see how they're applied to create cohesive components that respond to their environment.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">CSS Custom Properties Design System</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n\t&lt;div class=&quot;toolbar&quot;&gt;\n\t\t&lt;h1&gt;Design System with CSS Custom Properties&lt;/h1&gt;\n\t\t&lt;button id=&quot;theme-toggle&quot;&gt;\n\t\t\t&lt;span id=&quot;theme-icon&quot;&gt;🌓&lt;/span&gt;\n\t\t\t&lt;span id=&quot;theme-text&quot;&gt;Toggle Theme&lt;/span&gt;\n\t\t&lt;/button&gt;\n\t&lt;/div&gt;\n\n\t&lt;p&gt;\n\t\tThis demo shows how Web Components can use CSS Custom Properties to create a consistent\n\t\tdesign system that responds to light/dark mode preferences and supports theming.\n\t&lt;/p&gt;\n\n\t&lt;div class=&quot;demo-section&quot;&gt;\n\t\t&lt;h2&gt;Color Tokens&lt;/h2&gt;\n\t\t&lt;p&gt;The foundation of our design system starts with color tokens:&lt;/p&gt;\n\n\t\t&lt;div class=&quot;token-grid&quot;&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--theme-primary);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--theme-primary&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;#4361ee&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--theme-secondary);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--theme-secondary&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;#3f37c9&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--theme-accent);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--theme-accent&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;#4cc9f0&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--theme-success);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--theme-success&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;#4caf50&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--theme-warning);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--theme-warning&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;#ff9800&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--theme-danger);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--theme-danger&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;#f72585&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\n\t\t&lt;h3&gt;Semantic Colors&lt;/h3&gt;\n\t\t&lt;p&gt;These adapt automatically between light and dark mode:&lt;/p&gt;\n\n\t\t&lt;div class=&quot;token-grid&quot;&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--surface-bg);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--surface-bg&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;(dynamic)&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--surface-card);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--surface-card&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;(dynamic)&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--text-primary);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--text-primary&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;(dynamic)&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t\t&lt;div class=&quot;token-item&quot;&gt;\n\t\t\t\t&lt;div class=&quot;color-preview&quot; style=&quot;background-color: var(--border-color);&quot;&gt;&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-name&quot;&gt;--border-color&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;token-value&quot;&gt;(dynamic)&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n\n\t&lt;div class=&quot;demo-section&quot;&gt;\n\t\t&lt;h2&gt;Component Examples&lt;/h2&gt;\n\t\t&lt;p&gt;These web components use our design system tokens to maintain consistency:&lt;/p&gt;\n\n\t\t&lt;div class=&quot;component-grid&quot;&gt;\n\t\t\t&lt;ds-card&gt;\n\t\t\t\t&lt;h3 slot=&quot;title&quot;&gt;Design System Card&lt;/h3&gt;\n\t\t\t\t&lt;p slot=&quot;content&quot;&gt;This card component automatically adapts to light/dark mode and uses our spacing, typography, and color tokens.&lt;/p&gt;\n\t\t\t\t&lt;div slot=&quot;footer&quot;&gt;\n\t\t\t\t\t&lt;ds-button variant=&quot;primary&quot;&gt;Primary Action&lt;/ds-button&gt;\n\t\t\t\t\t&lt;ds-button variant=&quot;secondary&quot;&gt;Secondary&lt;/ds-button&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/ds-card&gt;\n\n\t\t\t&lt;ds-card&gt;\n\t\t\t\t&lt;h3 slot=&quot;title&quot;&gt;Interactive Controls&lt;/h3&gt;\n\t\t\t\t&lt;div slot=&quot;content&quot;&gt;\n\t\t\t\t\t&lt;p&gt;Try these buttons that follow our design system:&lt;/p&gt;\n\t\t\t\t\t&lt;div style=&quot;display: flex; gap: var(--spacing-sm); flex-wrap: wrap; margin-bottom: var(--spacing-md);&quot;&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;primary&quot;&gt;Primary&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;secondary&quot;&gt;Secondary&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;accent&quot;&gt;Accent&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;success&quot;&gt;Success&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;warning&quot;&gt;Warning&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;danger&quot;&gt;Danger&lt;/ds-button&gt;\n\t\t\t\t\t&lt;/div&gt;\n\t\t\t\t\t&lt;p&gt;Size variants:&lt;/p&gt;\n\t\t\t\t\t&lt;div style=&quot;display: flex; gap: var(--spacing-sm); align-items: center;&quot;&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;primary&quot; size=&quot;small&quot;&gt;Small&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;primary&quot; size=&quot;medium&quot;&gt;Medium&lt;/ds-button&gt;\n\t\t\t\t\t\t&lt;ds-button variant=&quot;primary&quot; size=&quot;large&quot;&gt;Large&lt;/ds-button&gt;\n\t\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/ds-card&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Design System Tokens */\n:root {\n\t/* Theme Colors */\n\t--theme-primary: #4361ee;\n\t--theme-secondary: #3f37c9;\n\t--theme-accent: #4cc9f0;\n\t--theme-success: #4caf50;\n\t--theme-warning: #ff9800;\n\t--theme-danger: #f72585;\n\n\t/* Neutral Colors - Light Mode Default */\n\t--neutral-50: #f8f9fa;\n\t--neutral-100: #f0f0f0;\n\t--neutral-200: #e9ecef;\n\t--neutral-300: #dee2e6;\n\t--neutral-400: #ced4da;\n\t--neutral-500: #adb5bd;\n\t--neutral-600: #6c757d;\n\t--neutral-700: #495057;\n\t--neutral-800: #343a40;\n\t--neutral-900: #212529;\n\n\t/* Semantic Mappings - Light Mode */\n\t--surface-bg: var(--neutral-50);\n\t--surface-card: white;\n\t--text-primary: var(--neutral-900);\n\t--text-secondary: var(--neutral-700);\n\t--text-tertiary: var(--neutral-600);\n\t--border-color: var(--neutral-300);\n\n\t/* Spacing System */\n\t--spacing-unit: 4px;\n\t--spacing-xs: calc(var(--spacing-unit) * 1); /* 4px */\n\t--spacing-sm: calc(var(--spacing-unit) * 2); /* 8px */\n\t--spacing-md: calc(var(--spacing-unit) * 4); /* 16px */\n\t--spacing-lg: calc(var(--spacing-unit) * 6); /* 24px */\n\t--spacing-xl: calc(var(--spacing-unit) * 10); /* 40px */\n\n\t/* Typography */\n\t--font-family: system-ui, -apple-system, sans-serif;\n\t--font-size-xs: 12px;\n\t--font-size-sm: 14px;\n\t--font-size-md: 16px;\n\t--font-size-lg: 20px;\n\t--font-size-xl: 24px;\n\t--font-size-2xl: 32px;\n\n\t/* Border Radius */\n\t--radius-sm: 4px;\n\t--radius-md: 8px;\n\t--radius-lg: 16px;\n\t--radius-xl: 24px;\n\t--radius-full: 9999px;\n\n\t/* Shadows */\n\t--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);\n\t--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);\n\t--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);\n\n\t/* Animation */\n\t--duration-fast: 150ms;\n\t--duration-normal: 300ms;\n\t--duration-slow: 500ms;\n\t--easing-standard: cubic-bezier(0.4, 0, 0.2, 1);\n\n\t/* Component Specific - Button */\n\t--button-padding-x: var(--spacing-md);\n\t--button-padding-y: var(--spacing-sm);\n\t--button-radius: var(--radius-md);\n\t--button-font-size: var(--font-size-md);\n\n\t/* Component Specific - Card */\n\t--card-padding: var(--spacing-md);\n\t--card-radius: var(--radius-md);\n\t--card-shadow: var(--shadow-md);\n\t--card-border: 1px solid var(--border-color);\n\n\tcolor-scheme: light dark;\n}\n\n/* Dark Mode Overrides */\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t/* Semantic Mappings - Dark Mode */\n\t\t--surface-bg: var(--neutral-900);\n\t\t--surface-card: var(--neutral-800);\n\t\t--text-primary: var(--neutral-100);\n\t\t--text-secondary: var(--neutral-300);\n\t\t--text-tertiary: var(--neutral-400);\n\t\t--border-color: var(--neutral-700);\n\t}\n}\n\n/* Base Styles */\nbody {\n\tfont-family: var(--font-family);\n\tbackground-color: var(--surface-bg);\n\tcolor: var(--text-primary);\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: var(--spacing-md);\n\ttransition: background-color var(--duration-normal) var(--easing-standard);\n}\n\n.container {\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n\tcolor: var(--text-primary);\n\tmargin-top: 0;\n}\n\nh1 {\n\tfont-size: var(--font-size-2xl);\n\tmargin-bottom: var(--spacing-lg);\n}\n\nh2 {\n\tfont-size: var(--font-size-xl);\n\tmargin-bottom: var(--spacing-md);\n\tmargin-top: var(--spacing-xl);\n}\n\nh3 {\n\tfont-size: var(--font-size-lg);\n\tmargin-bottom: var(--spacing-sm);\n}\n\np {\n\tmargin-bottom: var(--spacing-md);\n\tcolor: var(--text-secondary);\n}\n\nbutton {\n\tcursor: pointer;\n}\n\n.demo-section {\n\tbackground-color: var(--surface-card);\n\tborder-radius: var(--radius-md);\n\tpadding: var(--spacing-lg);\n\tmargin-bottom: var(--spacing-xl);\n\tborder: 1px solid var(--border-color);\n}\n\n.token-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\n\tgap: var(--spacing-md);\n\tmargin: var(--spacing-lg) 0;\n}\n\n.token-item {\n\tpadding: var(--spacing-md);\n\tborder-radius: var(--radius-sm);\n\tborder: 1px solid var(--border-color);\n\tbackground-color: var(--surface-card);\n}\n\n.token-name {\n\tfont-family: monospace;\n\tfont-size: var(--font-size-sm);\n\tcolor: var(--theme-primary);\n\tmargin-bottom: var(--spacing-xs);\n}\n\n.token-value {\n\tfont-family: monospace;\n\tfont-size: var(--font-size-sm);\n\tcolor: var(--text-tertiary);\n}\n\n.color-preview {\n\theight: 30px;\n\tborder-radius: var(--radius-sm);\n\tmargin-bottom: var(--spacing-xs);\n}\n\n.component-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n\tgap: var(--spacing-xl);\n\tmargin: var(--spacing-lg) 0;\n}\n\n.toolbar {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: center;\n\tmargin-bottom: var(--spacing-lg);\n}\n\n#theme-toggle {\n\tbackground-color: var(--surface-card);\n\tcolor: var(--text-primary);\n\tborder: 1px solid var(--border-color);\n\tpadding: var(--spacing-sm) var(--spacing-md);\n\tborder-radius: var(--radius-full);\n\tfont-size: var(--font-size-sm);\n\tdisplay: flex;\n\talign-items: center;\n\tgap: var(--spacing-xs);\n\ttransition: all var(--duration-fast) var(--easing-standard);\n}\n\n#theme-toggle:hover {\n\tbackground-color: var(--neutral-200);\n}\n\n@media (prefers-color-scheme: dark) {\n\t#theme-toggle:hover {\n\t\tbackground-color: var(--neutral-700);\n\t}\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Design System Button Component\nclass DesignSystemButton extends HTMLElement {\n  static get observedAttributes() {\n    return [&quot;variant&quot;, &quot;size&quot;, &quot;disabled&quot;];\n  }\n\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this.render();\n  }\n\n  connectedCallback() {\n    this.shadowRoot.querySelector(&quot;button&quot;).addEventListener(&quot;click&quot;, e =&gt; {\n      if (!this.hasAttribute(&quot;disabled&quot;)) {\n        this.dispatchEvent(\n        new CustomEvent(&quot;click&quot;, {\n          bubbles: true,\n          composed: true,\n          detail: { originalEvent: e } }));\n\n\n      }\n    });\n  }\n\n  attributeChangedCallback() {\n    this.render();\n  }\n\n  disconnectedCallback() {\n    this.shadowRoot.\n    querySelector(&quot;button&quot;).\n    removeEventListener(&quot;click&quot;, this.handleClick);\n  }\n\n  render() {\n    const variant = this.getAttribute(&quot;variant&quot;) || &quot;primary&quot;;\n    const size = this.getAttribute(&quot;size&quot;) || &quot;medium&quot;;\n    const disabled = this.hasAttribute(&quot;disabled&quot;);\n\n    const buttonSizes = {\n      small: {\n        &quot;--button-font-size&quot;: &quot;var(--font-size-sm)&quot;,\n        &quot;--button-padding&quot;: &quot;var(--spacing-xs) var(--spacing-sm)&quot; },\n\n      medium: {\n        &quot;--button-font-size&quot;: &quot;var(--font-size-md)&quot;,\n        &quot;--button-padding&quot;: &quot;var(--spacing-sm) var(--spacing-md)&quot; },\n\n      large: {\n        &quot;--button-font-size&quot;: &quot;var(--font-size-lg)&quot;,\n        &quot;--button-padding&quot;: &quot;var(--spacing-md) var(--spacing-lg)&quot; } };\n\n\n\n    const sizeStyle = buttonSizes[size] || buttonSizes.medium;\n\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: inline-block;\n            }\n            \n            button {\n              font-family: var(--font-family);\n              font-size: var(--button-font-size);\n              padding: var(--button-padding);\n              border-radius: var(--button-radius, var(--radius-md));\n              border: none;\n              cursor: ${disabled ? &quot;not-allowed&quot; : &quot;pointer&quot;};\n              opacity: ${disabled ? &quot;0.6&quot; : &quot;1&quot;};\n              transition: all var(--duration-fast) var(--easing-standard);\n              color: white;\n              background-color: var(--theme-${variant}, var(--theme-primary));\n            }\n            \n            button.secondary {\n              background-color: transparent;\n              color: var(--theme-${variant}, var(--theme-primary));\n              border: 1px solid var(--theme-${variant}, var(--theme-primary));\n            }\n            \n            button:hover:not(:disabled) {\n              filter: brightness(1.1);\n              transform: translateY(-1px);\n            }\n            \n            button:active:not(:disabled) {\n              filter: brightness(0.9);\n              transform: translateY(0);\n            }\n            \n            button:focus-visible {\n              outline: 2px solid var(--theme-${variant}, var(--theme-primary));\n              outline-offset: 2px;\n            }\n          &lt;/style&gt;\n          &lt;button \n            class=&quot;${variant === &quot;secondary&quot; ? &quot;secondary&quot; : &quot;&quot;}&quot; \n            ?disabled=&quot;${disabled}&quot;\n            style=&quot;${Object.entries(sizeStyle).\n    map(([prop, val]) =&gt; `${prop}: ${val}`).\n    join(&quot;;&quot;)}&quot;&gt;\n            &lt;slot&gt;&lt;/slot&gt;\n          &lt;/button&gt;\n        `;\n  }}\n\n\n// Design System Card Component\nclass DesignSystemCard extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n    this.render();\n  }\n\n  render() {\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n            }\n            \n            .card {\n              background-color: var(--surface-card);\n              border-radius: var(--card-radius);\n              box-shadow: var(--card-shadow);\n              border: var(--card-border);\n              overflow: hidden;\n              transition: all var(--duration-normal) var(--easing-standard);\n            }\n            \n            .card:hover {\n              box-shadow: var(--shadow-lg);\n              transform: translateY(-2px);\n            }\n            \n            .card-header {\n              padding: var(--card-padding);\n              border-bottom: 1px solid var(--border-color);\n            }\n            \n            .card-body {\n              padding: var(--card-padding);\n            }\n            \n            .card-footer {\n              padding: var(--card-padding);\n              border-top: 1px solid var(--border-color);\n              display: flex;\n              gap: var(--spacing-sm);\n              flex-wrap: wrap;\n            }\n            \n            ::slotted(h3) {\n              margin-top: 0;\n              margin-bottom: 0;\n            }\n          &lt;/style&gt;\n          &lt;div class=&quot;card&quot;&gt;\n            &lt;div class=&quot;card-header&quot;&gt;\n              &lt;slot name=&quot;title&quot;&gt;&lt;/slot&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;card-body&quot;&gt;\n              &lt;slot name=&quot;content&quot;&gt;&lt;/slot&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;card-footer&quot;&gt;\n              &lt;slot name=&quot;footer&quot;&gt;&lt;/slot&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        `;\n  }}\n\n\n// Register custom elements\ncustomElements.define(&quot;ds-button&quot;, DesignSystemButton);\ncustomElements.define(&quot;ds-card&quot;, DesignSystemCard);\n\n// Theme toggle functionality\ndocument.getElementById(&quot;theme-toggle&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const prefersDark = window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;\n  const rootStyle = document.documentElement.style;\n\n  // If system prefers dark and we&#39;re in dark mode OR system prefers light and we&#39;re in light mode\n  const isUsingSystemTheme = !rootStyle.getPropertyValue(&quot;--surface-bg&quot;);\n\n  if (isUsingSystemTheme) {\n    if (prefersDark) {\n      // System prefers dark, switch to light\n      rootStyle.setProperty(&quot;--surface-bg&quot;, &quot;var(--neutral-50)&quot;);\n      rootStyle.setProperty(&quot;--surface-card&quot;, &quot;white&quot;);\n      rootStyle.setProperty(&quot;--text-primary&quot;, &quot;var(--neutral-900)&quot;);\n      rootStyle.setProperty(&quot;--text-secondary&quot;, &quot;var(--neutral-700)&quot;);\n      rootStyle.setProperty(&quot;--text-tertiary&quot;, &quot;var(--neutral-600)&quot;);\n      rootStyle.setProperty(&quot;--border-color&quot;, &quot;var(--neutral-300)&quot;);\n      document.getElementById(&quot;theme-text&quot;).textContent = &quot;Light Mode&quot;;\n      document.getElementById(&quot;theme-icon&quot;).textContent = &quot;☀️&quot;;\n    } else {\n      // System prefers light, switch to dark\n      rootStyle.setProperty(&quot;--surface-bg&quot;, &quot;var(--neutral-900)&quot;);\n      rootStyle.setProperty(&quot;--surface-card&quot;, &quot;var(--neutral-800)&quot;);\n      rootStyle.setProperty(&quot;--text-primary&quot;, &quot;var(--neutral-100)&quot;);\n      rootStyle.setProperty(&quot;--text-secondary&quot;, &quot;var(--neutral-300)&quot;);\n      rootStyle.setProperty(&quot;--text-tertiary&quot;, &quot;var(--neutral-400)&quot;);\n      rootStyle.setProperty(&quot;--border-color&quot;, &quot;var(--neutral-700)&quot;);\n      document.getElementById(&quot;theme-text&quot;).textContent = &quot;Dark Mode&quot;;\n      document.getElementById(&quot;theme-icon&quot;).textContent = &quot;🌙&quot;;\n    }\n  } else {\n    // We&#39;re using a manual theme, reset to system preference\n    rootStyle.removeProperty(&quot;--surface-bg&quot;);\n    rootStyle.removeProperty(&quot;--surface-card&quot;);\n    rootStyle.removeProperty(&quot;--text-primary&quot;);\n    rootStyle.removeProperty(&quot;--text-secondary&quot;);\n    rootStyle.removeProperty(&quot;--text-tertiary&quot;);\n    rootStyle.removeProperty(&quot;--border-color&quot;);\n    document.getElementById(&quot;theme-text&quot;).textContent = &quot;System Theme&quot;;\n    document.getElementById(&quot;theme-icon&quot;).textContent = &quot;🌓&quot;;\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<p><strong>Challenge: override a token from the page.</strong> The card below reads its padding from <code>--spacing-md</code>, falling back to 16px. Run it, then add one rule to the page CSS (the TODO) that sets <code>--spacing-md</code> on <code>my-card</code>. Expected result: the card's padding grows to 40px and the whole card gets visibly roomier. Custom properties are inherited, so they travel from the page into the shadow tree even though ordinary selectors cannot.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Challenge: override --spacing-md</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;my-card&gt;&lt;p&gt;Hello from the light DOM.&lt;/p&gt;&lt;/my-card&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\n/* TODO: add  my-card { --spacing-md: 40px; }  below this line */\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class MyCard extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; }).innerHTML = `\n      &lt;style&gt;\n        .box {\n          padding: var(--spacing-md, 16px);\n          border: 2px solid steelblue;\n        }\n      &lt;/style&gt;\n      &lt;div class=&quot;box&quot;&gt;&lt;slot&gt;&lt;/slot&gt;&lt;/div&gt;`;\n  }\n}\ncustomElements.define(&#39;my-card&#39;, MyCard);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>The rule goes in the CSS tab, not inside the component. Try <code>--spacing-md: 4px</code> too and watch the card shrink.</p>\n</aside>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A page has the rules <code>my-card { … }</code>, <code>my-card p { … }</code> and <code>my-card .box { … }</code>. The <code>&lt;p&gt;</code> is slotted light DOM, and <code>.box</code> is a <code>&lt;div&gt;</code> inside the component's shadow tree. Which elements can these page rules style?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>Only the <code>my-card</code> host</li>\n<li>The <code>my-card</code> host and the slotted <code>&lt;p&gt;</code>, but not the inner <code>.box</code></li>\n<li>All three, because the page's CSS wins over the component's</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. The <code>my-card</code> host and the slotted <code>&lt;p&gt;</code>, but not the inner <code>.box</code></p>\n<p>The host and its light DOM children belong to the page's own tree. The inner <code>.box</code> lives in the shadow tree, so page selectors never match it.</p>\n</div>\n</div>\n</section>\n<h2 id=\"the-fourth-door-part\">The Fourth Door: <code>::part</code><a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#the-fourth-door-part\" aria-label=\"Link to this section\">#</a></h2>\n<p>Custom properties work only when the component chose to read a variable. <code>::part</code> is the other direction: the component names an inner element with a <code>part</code> attribute, and the page can then style that element directly.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Styling a named part from the page</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;my-card&gt;&lt;p&gt;Slotted text&lt;/p&gt;&lt;/my-card&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\nmy-card::part(action) {\n  background: tomato;\n  color: white;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class MyCard extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &#39;open&#39; }).innerHTML = `\n      &lt;slot&gt;&lt;/slot&gt;\n      &lt;button part=&quot;action&quot;&gt;Save&lt;/button&gt;`;\n  }\n}\ncustomElements.define(&#39;my-card&#39;, MyCard);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<p>Delete the <code>part=\"action\"</code> attribute and run again: the button goes back to its default look, because the page can only reach what the component exposes. One limit worth knowing: <code>::part(action)</code> styles that element itself, not its children, so <code>::part(action) span</code> matches nothing.</p>\n<h2 id=\"inheritance-and-context-adaptation-techniques\">Inheritance and Context Adaptation Techniques<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#inheritance-and-context-adaptation-techniques\" aria-label=\"Link to this section\">#</a></h2>\n<p>For components that need to adapt to their environment while maintaining encapsulation, sophisticated context-handling techniques can be employed:</p>\n<pre><code class=\"language-javascript\">class ContextAwareComponent extends HTMLElement {\n  constructor() {\n    super();\n    const shadow = this.attachShadow({ mode: 'open' });\n\n    shadow.innerHTML = `\n      &lt;style&gt;\n        /* Base responsive container that adapts to context */\n        :host {\n          display: block;\n          /* Inherit typography properties purposefully */\n          font-family: inherit;\n          font-size: inherit;\n          line-height: inherit;\n          color: inherit;\n          /* Default containment for performance */\n          contain: content;\n        }\n\n        /* Contextual adaptation */\n        :host([data-context=\"card\"]) .content {\n          padding: var(--card-inner-spacing, 16px);\n        }\n\n        :host([data-context=\"sidebar\"]) .content {\n          padding: var(--sidebar-item-spacing, 8px 12px);\n        }\n\n        /* Content styles */\n        .content {\n          background-color: var(--component-bg, transparent);\n          border-radius: var(--component-radius, 4px);\n          transition: background-color var(--duration-normal, 300ms) ease;\n        }\n\n        /* Advanced slotted content styling */\n        ::slotted(h1),\n        ::slotted(h2),\n        ::slotted(h3) {\n          margin-top: 0;\n          color: var(--heading-color, currentColor);\n        }\n\n        ::slotted(p) {\n          margin-bottom: var(--paragraph-spacing, 1em);\n        }\n      &lt;/style&gt;\n      &lt;div class=\"content\"&gt;\n        &lt;slot&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n\n    // Context detection for environment-specific styling\n    this._detectContext();\n\n    // MutationObserver for dynamic context changes\n    this._observer = new MutationObserver(() =&gt; this._detectContext());\n    this._observer.observe(this, { attributes: true });\n  }\n\n  connectedCallback() {\n    // Re-evaluate context when the component is inserted into a new location\n    this._detectContext();\n    this._startContextObservation();\n  }\n\n  disconnectedCallback() {\n    this._observer.disconnect();\n  }\n\n  _startContextObservation() {\n    // Walk up the DOM tree to find parent components that might change\n    let parent = this.parentElement;\n    while (parent) {\n      if (parent.tagName?.includes('-')) {\n        this._observer.observe(parent, { attributes: true });\n      }\n      parent = parent.parentElement;\n    }\n  }\n\n  _detectContext() {\n    // Detect where this component is being used\n    const parentCard = this.closest('card-component');\n    const parentSidebar = this.closest('sidebar-component');\n\n    if (parentCard) {\n      this.setAttribute('data-context', 'card');\n    } else if (parentSidebar) {\n      this.setAttribute('data-context', 'sidebar');\n    } else {\n      this.removeAttribute('data-context');\n    }\n  }\n}\n\ncustomElements.define('context-aware-component', ContextAwareComponent);\n</code></pre>\n<p>This implementation demonstrates advanced environment adaptation techniques:</p>\n<ul>\n<li>\n<p>Selective inheritance of typography properties</p>\n</li>\n<li>\n<p>Context detection through DOM traversal</p>\n</li>\n<li>\n<p>Dynamic adaptation through attribute-based styling</p>\n</li>\n<li>\n<p>MutationObserver for responding to contextual changes</p>\n</li>\n</ul>\n<p>This demonstration reveals how sophisticated components can intelligently adapt to their surrounding context. The same component automatically adjusts its appearance when placed in different containers (cards vs. sidebars) and responds to theme changes without requiring manual style updates. The implementation uses MutationObserver to detect DOM changes and dynamically updates styling based on the component's environment. Try toggling between container types to see how the component repositions and restyling itself, and switch between light and dark modes to observe theme adaptation in action.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Context-Aware Web Components</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n\t&lt;div class=&quot;title-section&quot;&gt;\n\t\t&lt;h1&gt;Context-Aware Web Components&lt;/h1&gt;\n\t\t&lt;p&gt;Demonstrating how Web Components can adapt to their environment&lt;/p&gt;\n\t&lt;/div&gt;\n\n\t&lt;div class=&quot;demo-section&quot;&gt;\n\t\t&lt;h2&gt;Component Adaptation to Parent Context&lt;/h2&gt;\n\t\t&lt;p&gt;The same component adapts its styling based on where it&#39;s placed in the UI.&lt;/p&gt;\n\n\t\t&lt;div class=&quot;card-container&quot;&gt;\n\t\t\t&lt;div class=&quot;card&quot; id=&quot;card-context&quot;&gt;\n\t\t\t\t&lt;div class=&quot;card-header&quot;&gt;\n\t\t\t\t\t&lt;h3&gt;Card Context&lt;/h3&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;card-body&quot;&gt;\n\t\t\t\t\t&lt;p&gt;This panel contains a context-aware component in a card:&lt;/p&gt;\n\t\t\t\t\t&lt;context-aware-component&gt;\n\t\t\t\t\t\t&lt;h4&gt;Inside a Card&lt;/h4&gt;\n\t\t\t\t\t\t&lt;p&gt;This component detects it&#39;s inside a card and styles accordingly.&lt;/p&gt;\n\t\t\t\t\t&lt;/context-aware-component&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\n\t\t\t&lt;div class=&quot;sidebar&quot; id=&quot;sidebar-context&quot;&gt;\n\t\t\t\t&lt;div class=&quot;sidebar-header&quot;&gt;\n\t\t\t\t\t&lt;h3&gt;Sidebar Context&lt;/h3&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;sidebar-items&quot;&gt;\n\t\t\t\t\t&lt;p&gt;The same component in a sidebar:&lt;/p&gt;\n\t\t\t\t\t&lt;context-aware-component&gt;\n\t\t\t\t\t\t&lt;h4&gt;Inside a Sidebar&lt;/h4&gt;\n\t\t\t\t\t\t&lt;p&gt;This component detects it&#39;s inside a sidebar and adjusts its appearance.&lt;/p&gt;\n\t\t\t\t\t&lt;/context-aware-component&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n\n\t&lt;div class=&quot;demo-section&quot;&gt;\n\t\t&lt;h2&gt;Component Adaptation to Theme Changes&lt;/h2&gt;\n\t\t&lt;p&gt;The component automatically adapts to light/dark mode and react to runtime theme changes.&lt;/p&gt;\n\n\t\t&lt;div class=&quot;card-container&quot;&gt;\n\t\t\t&lt;div class=&quot;card&quot;&gt;\n\t\t\t\t&lt;div class=&quot;card-header&quot;&gt;\n\t\t\t\t\t&lt;h3&gt;Default Context&lt;/h3&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;card-body&quot;&gt;\n\t\t\t\t\t&lt;context-aware-component&gt;\n\t\t\t\t\t\t&lt;h4&gt;Default Styling&lt;/h4&gt;\n\t\t\t\t\t\t&lt;p&gt;This component is in a standard context and will adapt to light/dark mode.&lt;/p&gt;\n\t\t\t\t\t&lt;/context-aware-component&gt;\n\n\t\t\t\t\t&lt;div class=&quot;theme-toggle&quot;&gt;\n\t\t\t\t\t\t&lt;button id=&quot;theme-toggle&quot;&gt;Toggle Light/Dark Mode&lt;/button&gt;\n\t\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n\n\t&lt;div class=&quot;demo-section&quot;&gt;\n\t\t&lt;h2&gt;DOM Hierarchy Observation&lt;/h2&gt;\n\t\t&lt;p&gt;The component will update its context when parent elements change.&lt;/p&gt;\n\n\t\t&lt;div class=&quot;card-container&quot;&gt;\n\t\t\t&lt;div id=&quot;dynamic-container&quot; class=&quot;card&quot;&gt;\n\t\t\t\t&lt;div class=&quot;card-header&quot;&gt;\n\t\t\t\t\t&lt;h3&gt;Dynamic Context&lt;/h3&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;card-body&quot;&gt;\n\t\t\t\t\t&lt;context-aware-component&gt;\n\t\t\t\t\t\t&lt;h4&gt;Dynamic Content&lt;/h4&gt;\n\t\t\t\t\t\t&lt;p&gt;Watch this component adapt when its container changes.&lt;/p&gt;\n\t\t\t\t\t&lt;/context-aware-component&gt;\n\n\t\t\t\t\t&lt;div class=&quot;theme-toggle&quot;&gt;\n\t\t\t\t\t\t&lt;button id=&quot;toggle-container&quot;&gt;Toggle Container Type&lt;/button&gt;\n\t\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Design System Variables */\n:root {\n\t/* Light Theme (Default) */\n\t--color-bg: #f8f9fa;\n\t--color-surface: #ffffff;\n\t--color-text: #212529;\n\t--color-text-secondary: #6c757d;\n\t--color-border: #dee2e6;\n\t--color-primary: #0d6efd;\n\t--color-success: #198754;\n\t--color-danger: #dc3545;\n\t--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);\n\t--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);\n\n\t/* For detecting dark mode in the JavaScript */\n\t--theme-id: &quot;light&quot;;\n\tcolor-scheme: light dark;\n}\n\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t/* Dark Theme */\n\t\t--color-bg: #212529;\n\t\t--color-surface: #343a40;\n\t\t--color-text: #f8f9fa;\n\t\t--color-text-secondary: #adb5bd;\n\t\t--color-border: #495057;\n\t\t--color-primary: #0d6efd;\n\t\t--color-success: #198754;\n\t\t--color-danger: #dc3545;\n\t\t--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);\n\t\t--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);\n\n\t\t/* For detecting dark mode in the JavaScript */\n\t\t--theme-id: &quot;dark&quot;;\n\t}\n}\n\n/* Base Styles */\nbody {\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\n\tbackground-color: var(--color-bg);\n\tcolor: var(--color-text);\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 20px;\n\ttransition: background-color 0.3s ease;\n}\n\n.container {\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\nh1,\nh2,\nh3,\nh4 {\n\tcolor: var(--color-text);\n\tmargin-top: 0;\n}\n\np {\n\tcolor: var(--color-text-secondary);\n}\n\n.title-section {\n\tmargin-bottom: 40px;\n\ttext-align: center;\n}\n\n.demo-section {\n\tmargin-bottom: 40px;\n\tpadding: 20px;\n\tbackground-color: var(--color-surface);\n\tborder-radius: 8px;\n\tbox-shadow: var(--shadow-sm);\n\tborder: 1px solid var(--color-border);\n}\n\n.card-container {\n\tmargin-top: 20px;\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n\tgap: 20px;\n}\n\n/* Card Styling */\n.card {\n\tbackground-color: var(--color-surface);\n\tborder-radius: 8px;\n\tborder: 1px solid var(--color-border);\n\toverflow: hidden;\n}\n\n.card-header {\n\tpadding: 15px;\n\tbackground-color: rgba(0, 0, 0, 0.03);\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.card-body {\n\tpadding: 15px;\n}\n\n/* Sidebar Styling */\n.sidebar {\n\tbackground-color: var(--color-surface);\n\tborder-radius: 8px;\n\tborder: 1px solid var(--color-border);\n\tpadding: 15px;\n}\n\n.sidebar-header {\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding-bottom: 10px;\n\tmargin-bottom: 15px;\n}\n\n.sidebar-items {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 5px;\n}\n\n.theme-toggle {\n\tmargin-top: 20px;\n\ttext-align: center;\n}\n\nbutton {\n\tbackground-color: var(--color-primary);\n\tcolor: white;\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\tfont-size: 14px;\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class ContextAwareComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.attachShadow({ mode: &quot;open&quot; });\n\n    // Render initial component\n    this.render();\n\n    // Initialize MutationObserver\n    this._observer = new MutationObserver(() =&gt; this._detectContext());\n  }\n\n  connectedCallback() {\n    // Detect context when added to DOM\n    this._detectContext();\n    this._startContextObservation();\n\n    // Listen for theme changes\n    window.addEventListener(&quot;theme-change&quot;, () =&gt; this._detectContext());\n  }\n\n  disconnectedCallback() {\n    // Clean up observer\n    this._observer.disconnect();\n    window.removeEventListener(&quot;theme-change&quot;, () =&gt; this._detectContext());\n  }\n\n  _startContextObservation() {\n    // Observe this element for attribute changes\n    this._observer.observe(this, { attributes: true });\n\n    // Walk up the DOM tree to find parent containers that might change\n    let parent = this.parentElement;\n    while (parent) {\n      // If the parent is a custom element or one of our contexts, observe it\n      if (\n      parent.tagName?.includes(&quot;-&quot;) ||\n      parent.id === &quot;card-context&quot; ||\n      parent.id === &quot;sidebar-context&quot; ||\n      parent.id === &quot;dynamic-container&quot;)\n      {\n        this._observer.observe(parent, { attributes: true });\n      }\n      parent = parent.parentElement;\n    }\n  }\n\n  _detectContext() {\n    // Reset context\n    this.removeAttribute(&quot;data-context&quot;);\n\n    // Detect container context\n    if (this.closest(&quot;#card-context, .card&quot;)) {\n      this.setAttribute(&quot;data-context&quot;, &quot;card&quot;);\n    } else if (this.closest(&quot;#sidebar-context, .sidebar&quot;)) {\n      this.setAttribute(&quot;data-context&quot;, &quot;sidebar&quot;);\n    }\n\n    // Detect theme context\n    const computedStyle = getComputedStyle(document.documentElement);\n    const themeId = computedStyle.\n    getPropertyValue(&quot;--theme-id&quot;).\n    trim().\n    replace(/&quot;/g, &quot;&quot;);\n\n    this.setAttribute(&quot;data-theme&quot;, themeId);\n\n    // Re-render with new context\n    this.render();\n  }\n\n  render() {\n    const context = this.getAttribute(&quot;data-context&quot;) || &quot;default&quot;;\n    const theme = this.getAttribute(&quot;data-theme&quot;) || &quot;light&quot;;\n\n    // Context-specific styling\n    const contextStyles = {\n      default: `\n            padding: 15px;\n            border-radius: 4px;\n            background-color: var(--color-surface);\n            border: 1px solid var(--color-border);\n          `,\n      card: `\n            padding: 20px;\n            border-radius: 8px;\n            background-color: rgba(0,0,0,0.03);\n            border: 1px solid var(--color-border);\n            margin: 10px 0;\n          `,\n      sidebar: `\n            padding: 10px;\n            border-left: 3px solid var(--color-primary);\n            background-color: transparent;\n            margin: 5px 0;\n          ` };\n\n\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              transition: all 0.3s ease;\n            }\n            \n            /* Base responsive container that adapts to context */\n            .content {\n              ${contextStyles[context]}\n              color: var(--color-text);\n              /* Inherit typography properties purposefully */\n              font-family: inherit;\n              font-size: inherit;\n              line-height: inherit;\n            }\n            \n            /* Container animations based on context */\n            :host([data-context=&quot;card&quot;]) .content:hover {\n              box-shadow: var(--shadow-md);\n              transform: translateY(-2px);\n            }\n            \n            :host([data-context=&quot;sidebar&quot;]) .content:hover {\n              background-color: rgba(13, 110, 253, 0.05);\n            }\n            \n            /* Theme-specific styling */\n            :host([data-theme=&quot;dark&quot;]) .content {\n              box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n            }\n            \n            :host([data-theme=&quot;light&quot;]) .content {\n              box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n            }\n            \n            /* Advanced slotted content styling */\n            ::slotted(h4) {\n              margin-top: 0;\n              color: var(--color-text);\n              font-size: ${context === &quot;sidebar&quot; ? &quot;16px&quot; : &quot;18px&quot;};\n            }\n            \n            ::slotted(p) {\n              margin-bottom: 0;\n              color: var(--color-text-secondary);\n              font-size: ${context === &quot;sidebar&quot; ? &quot;14px&quot; : &quot;16px&quot;};\n            }\n            \n            .context-label {\n              display: inline-block;\n              font-size: 12px;\n              padding: 2px 6px;\n              border-radius: 4px;\n              margin-bottom: 8px;\n              background-color: var(--color-primary);\n              color: white;\n            }\n          &lt;/style&gt;\n          \n          &lt;div class=&quot;content&quot;&gt;\n            &lt;div class=&quot;context-label&quot;&gt;${context} context (${theme} theme)&lt;/div&gt;\n            &lt;slot&gt;&lt;/slot&gt;\n          &lt;/div&gt;\n        `;\n  }}\n\n\n// Register custom element\ncustomElements.define(&quot;context-aware-component&quot;, ContextAwareComponent);\n\n// Theme toggle functionality\ndocument.getElementById(&quot;theme-toggle&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const root = document.documentElement;\n  const computedStyle = getComputedStyle(root);\n  const currentTheme = computedStyle.\n  getPropertyValue(&quot;--theme-id&quot;).\n  trim().\n  replace(/&quot;/g, &quot;&quot;);\n\n  if (currentTheme === &quot;light&quot;) {\n    root.style.setProperty(&quot;--color-bg&quot;, &quot;#212529&quot;);\n    root.style.setProperty(&quot;--color-surface&quot;, &quot;#343a40&quot;);\n    root.style.setProperty(&quot;--color-text&quot;, &quot;#f8f9fa&quot;);\n    root.style.setProperty(&quot;--color-text-secondary&quot;, &quot;#adb5bd&quot;);\n    root.style.setProperty(&quot;--color-border&quot;, &quot;#495057&quot;);\n    root.style.setProperty(&quot;--theme-id&quot;, &#39;&quot;dark&quot;&#39;);\n  } else {\n    root.style.setProperty(&quot;--color-bg&quot;, &quot;#f8f9fa&quot;);\n    root.style.setProperty(&quot;--color-surface&quot;, &quot;#ffffff&quot;);\n    root.style.setProperty(&quot;--color-text&quot;, &quot;#212529&quot;);\n    root.style.setProperty(&quot;--color-text-secondary&quot;, &quot;#6c757d&quot;);\n    root.style.setProperty(&quot;--color-border&quot;, &quot;#dee2e6&quot;);\n    root.style.setProperty(&quot;--theme-id&quot;, &#39;&quot;light&quot;&#39;);\n  }\n\n  // Dispatch custom event for theme change\n  window.dispatchEvent(new CustomEvent(&quot;theme-change&quot;));\n});\n\n// Toggle container type\ndocument.getElementById(&quot;toggle-container&quot;).addEventListener(&quot;click&quot;, () =&gt; {\n  const container = document.getElementById(&quot;dynamic-container&quot;);\n\n  if (container.classList.contains(&quot;card&quot;)) {\n    container.classList.remove(&quot;card&quot;);\n    container.classList.add(&quot;sidebar&quot;);\n  } else {\n    container.classList.remove(&quot;sidebar&quot;);\n    container.classList.add(&quot;card&quot;);\n  }\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<h2 id=\"performance-optimization-strategies\">Performance Optimization Strategies<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#performance-optimization-strategies\" aria-label=\"Link to this section\">#</a></h2>\n<p>Optimal Web Component styling requires consideration of rendering performance, particularly when designing systems that may include hundreds of component instances.</p>\n<h3 id=\"critical-rendering-path-optimization\">Critical Rendering Path Optimization<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#critical-rendering-path-optimization\" aria-label=\"Link to this section\">#</a></h3>\n<pre><code class=\"language-javascript\">class PerformanceOptimizedComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.shadow = this.attachShadow({ mode: 'open' });\n\n    // Prioritize structural rendering\n    this._createDOMStructure();\n\n    // Defer non-critical styles\n    this._deferNonCriticalStyles();\n  }\n\n  _createDOMStructure() {\n    // Critical CSS for initial render\n    const criticalStyle = document.createElement('style');\n    criticalStyle.textContent = `\n      :host { display: block; }\n      .container { min-height: 20px; }\n    `;\n\n    const container = document.createElement('div');\n    container.className = 'container';\n    container.innerHTML = '&lt;slot&gt;&lt;/slot&gt;';\n\n    this.shadow.appendChild(criticalStyle);\n    this.shadow.appendChild(container);\n  }\n\n  _deferNonCriticalStyles() {\n    // Use requestIdleCallback for non-critical styling\n    requestIdleCallback(() =&gt; {\n      const enhancedStyle = document.createElement('style');\n      enhancedStyle.textContent = `\n        .container {\n          background-color: var(--bg-color, #f9f9f9);\n          border-radius: var(--radius, 4px);\n          box-shadow: var(--shadow, 0 2px 4px rgba(0,0,0,0.05));\n          padding: var(--padding, 16px);\n          transition: transform 150ms ease-out, box-shadow 150ms ease-out;\n        }\n\n        .container:hover {\n          transform: translateY(-2px);\n          box-shadow: var(--shadow-hover, 0 4px 8px rgba(0,0,0,0.1));\n        }\n\n        ::slotted(*) {\n          margin-bottom: var(--item-spacing, 8px);\n        }\n\n        ::slotted(*:last-child) {\n          margin-bottom: 0;\n        }\n      `;\n      this.shadow.appendChild(enhancedStyle);\n    });\n  }\n}\n\ncustomElements.define('performance-optimized-component', PerformanceOptimizedComponent);\n</code></pre>\n<p>This architecture demonstrates several performance optimization techniques:</p>\n<ul>\n<li>\n<p>Prioritization of critical rendering path styles</p>\n</li>\n<li>\n<p>Deferred loading of non-critical styles</p>\n</li>\n<li>\n<p>Use of requestIdleCallback for low-priority style enhancements</p>\n</li>\n<li>\n<p>Minimal initial render footprint</p>\n</li>\n</ul>\n<p>Performance matters when building production-grade Web Components. This demo visualizes the impact of critical rendering path optimization by showing how components can prioritize structural content while deferring non-critical styles. Watch as the component first renders with minimal styling for immediate user interaction, then progressively enhances with advanced styling features. The timeline visualization shows how the component minimizes initial render time while providing a rich interactive experience. Toggle between optimized and unoptimized versions to see the difference in rendering performance.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Web Component Performance Optimization</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;div class=&quot;container&quot;&gt;\n\t&lt;div class=&quot;title-section&quot;&gt;\n\t\t&lt;h1&gt;Performance Optimization in Web Components&lt;/h1&gt;\n\t\t&lt;p&gt;Demonstrating critical rendering path optimization techniques for production-ready components&lt;/p&gt;\n\t&lt;/div&gt;\n\n\t&lt;div class=&quot;demo-section&quot;&gt;\n\t\t&lt;h2&gt;Critical Rendering Path Optimization&lt;/h2&gt;\n\t\t&lt;p&gt;\n\t\t\tCompare optimized vs. unoptimized components to see how prioritizing critical styles\n\t\t\tand deferring non-essential rendering can dramatically improve perceived performance.\n\t\t&lt;/p&gt;\n\n\t\t&lt;div class=&quot;control-panel&quot;&gt;\n\t\t\t&lt;button id=&quot;load-both&quot;&gt;Load Both Components&lt;/button&gt;\n\t\t\t&lt;button id=&quot;reset-demo&quot; class=&quot;secondary&quot;&gt;Reset Demo&lt;/button&gt;\n\t\t\t&lt;button id=&quot;toggle-theme&quot; class=&quot;secondary&quot;&gt;Toggle Light/Dark Mode&lt;/button&gt;\n\t\t&lt;/div&gt;\n\n\t\t&lt;div class=&quot;component-row&quot;&gt;\n\t\t\t&lt;div class=&quot;component-container&quot;&gt;\n\t\t\t\t&lt;div class=&quot;component-label&quot;&gt;Optimized Component&lt;/div&gt;\n\t\t\t\t&lt;div id=&quot;optimized-container&quot;&gt;\n\t\t\t\t\t&lt;p&gt;&lt;i&gt;Click &quot;Load Both Components&quot; to begin the demo...&lt;/i&gt;&lt;/p&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\n\t\t\t&lt;div class=&quot;component-container&quot;&gt;\n\t\t\t\t&lt;div class=&quot;component-label&quot;&gt;Unoptimized Component&lt;/div&gt;\n\t\t\t\t&lt;div id=&quot;unoptimized-container&quot;&gt;\n\t\t\t\t\t&lt;p&gt;&lt;i&gt;Click &quot;Load Both Components&quot; to begin the demo...&lt;/i&gt;&lt;/p&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\n\t\t&lt;div class=&quot;timeline&quot;&gt;\n\t\t\t&lt;h3&gt;Rendering Timeline&lt;/h3&gt;\n\t\t\t&lt;p&gt;Visualizing critical rendering events to demonstrate performance differences&lt;/p&gt;\n\n\t\t\t&lt;div class=&quot;timeline-container&quot;&gt;\n\t\t\t\t&lt;div class=&quot;timeline-track&quot; id=&quot;timeline-track&quot;&gt;\n\t\t\t\t\t&lt;!-- Timeline markers will be added here --&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;timeline-labels&quot;&gt;\n\t\t\t\t\t&lt;span&gt;0ms&lt;/span&gt;\n\t\t\t\t\t&lt;span&gt;500ms&lt;/span&gt;\n\t\t\t\t\t&lt;span&gt;1000ms&lt;/span&gt;\n\t\t\t\t\t&lt;span&gt;1500ms&lt;/span&gt;\n\t\t\t\t\t&lt;span&gt;2000ms&lt;/span&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\n\t\t\t&lt;div class=&quot;stats-panel&quot;&gt;\n\t\t\t\t&lt;h4&gt;Performance Metrics&lt;/h4&gt;\n\t\t\t\t&lt;div class=&quot;stats-row&quot;&gt;\n\t\t\t\t\t&lt;span&gt;Initial Load (Critical CSS):&lt;/span&gt;\n\t\t\t\t\t&lt;span id=&quot;critical-timing&quot;&gt;-&lt;/span&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;stats-row&quot;&gt;\n\t\t\t\t\t&lt;span&gt;Optimized Component Fully Rendered:&lt;/span&gt;\n\t\t\t\t\t&lt;span id=&quot;optimized-timing&quot;&gt;-&lt;/span&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t\t&lt;div class=&quot;stats-row&quot;&gt;\n\t\t\t\t\t&lt;span&gt;Unoptimized Component Fully Rendered:&lt;/span&gt;\n\t\t\t\t\t&lt;span id=&quot;unoptimized-timing&quot;&gt;-&lt;/span&gt;\n\t\t\t\t&lt;/div&gt;\n\t\t\t&lt;/div&gt;\n\t\t&lt;/div&gt;\n\t&lt;/div&gt;\n&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">/* Design System Variables */\n:root {\n\t/* Light Theme (Default) */\n\t--color-bg: #f8f9fa;\n\t--color-surface: #ffffff;\n\t--color-text: #212529;\n\t--color-text-secondary: #6c757d;\n\t--color-border: #dee2e6;\n\t--color-primary: #0d6efd;\n\t--color-success: #198754;\n\t--color-warning: #ffc107;\n\t--color-danger: #dc3545;\n\t--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);\n\t--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);\n\tcolor-scheme: light dark;\n}\n\n@media (prefers-color-scheme: dark) {\n\t:root {\n\t\t/* Dark Theme */\n\t\t--color-bg: #212529;\n\t\t--color-surface: #343a40;\n\t\t--color-text: #f8f9fa;\n\t\t--color-text-secondary: #adb5bd;\n\t\t--color-border: #495057;\n\t\t--color-primary: #0d6efd;\n\t\t--color-success: #198754;\n\t\t--color-warning: #ffc107;\n\t\t--color-danger: #dc3545;\n\t\t--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);\n\t\t--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);\n\t}\n}\n\n/* Base Styles */\nbody {\n\tfont-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\n\tbackground-color: var(--color-bg);\n\tcolor: var(--color-text);\n\tline-height: 1.5;\n\tmargin: 0;\n\tpadding: 20px;\n\ttransition: background-color 0.3s ease;\n}\n\n.container {\n\tmax-width: 1200px;\n\tmargin: 0 auto;\n}\n\nh1,\nh2,\nh3,\nh4 {\n\tcolor: var(--color-text);\n\tmargin-top: 0;\n}\n\np {\n\tcolor: var(--color-text-secondary);\n}\n\n.title-section {\n\tmargin-bottom: 40px;\n\ttext-align: center;\n}\n\n.demo-section {\n\tmargin-bottom: 40px;\n\tpadding: 20px;\n\tbackground-color: var(--color-surface);\n\tborder-radius: 8px;\n\tbox-shadow: var(--shadow-sm);\n\tborder: 1px solid var(--color-border);\n}\n\n.control-panel {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n\tmargin-bottom: 20px;\n\tpadding: 15px;\n\tbackground-color: rgba(0, 0, 0, 0.03);\n\tborder-radius: 4px;\n}\n\n.component-row {\n\tdisplay: grid;\n\tgrid-template-columns: 1fr 1fr;\n\tgap: 20px;\n\tmargin-top: 20px;\n}\n\n@media (max-width: 768px) {\n\t.component-row {\n\t\tgrid-template-columns: 1fr;\n\t}\n}\n\n.component-container {\n\tpadding: 20px;\n\tborder-radius: 8px;\n\tborder: 1px solid var(--color-border);\n}\n\n.timeline {\n\tmargin-top: 30px;\n\tpadding: 20px;\n\tbackground-color: var(--color-surface);\n\tborder-radius: 8px;\n\tbox-shadow: var(--shadow-sm);\n\tborder: 1px solid var(--color-border);\n}\n\n.timeline-container {\n\tposition: relative;\n\theight: 120px;\n\tmargin-top: 20px;\n\toverflow: hidden;\n}\n\n.timeline-track {\n\tposition: relative;\n\theight: 80px;\n\tbackground-color: rgba(0, 0, 0, 0.05);\n\tborder-radius: 4px;\n}\n\n.timeline-marker {\n\tposition: absolute;\n\theight: 100%;\n\tbackground-color: rgba(13, 110, 253, 0.2);\n\tborder-left: 2px solid var(--color-primary);\n\tpadding-left: 5px;\n\tdisplay: flex;\n\talign-items: center;\n\tfont-size: 14px;\n\tcolor: var(--color-primary);\n}\n\n.timeline-labels {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tmargin-top: 5px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-secondary);\n}\n\n.timeline-tick {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 5px;\n\tbackground-color: var(--color-text-secondary);\n\tbottom: -5px;\n}\n\nbutton {\n\tbackground-color: var(--color-primary);\n\tcolor: white;\n\tborder: none;\n\tpadding: 8px 16px;\n\tborder-radius: 4px;\n\tcursor: pointer;\n\tfont-size: 14px;\n\ttransition: background-color 0.2s;\n}\n\nbutton:hover {\n\tbackground-color: #0b5ed7;\n}\n\nbutton.secondary {\n\tbackground-color: var(--color-surface);\n\tcolor: var(--color-text);\n\tborder: 1px solid var(--color-border);\n}\n\nbutton.secondary:hover {\n\tbackground-color: rgba(0, 0, 0, 0.05);\n}\n\n.stats-panel {\n\tmargin-top: 20px;\n\tpadding: 15px;\n\tbackground-color: rgba(0, 0, 0, 0.03);\n\tborder-radius: 4px;\n\tfont-size: 14px;\n}\n\n.stats-row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tmargin-bottom: 5px;\n}\n\n.theme-toggle {\n\tmargin-top: 20px;\n\ttext-align: center;\n}\n\n.component-label {\n\tfont-size: 12px;\n\tpadding: 4px 8px;\n\tbackground-color: rgba(13, 110, 253, 0.1);\n\tcolor: var(--color-primary);\n\tborder-radius: 4px;\n\tmargin-bottom: 10px;\n\tdisplay: inline-block;\n}\n\n.spinner {\n\tdisplay: inline-block;\n\twidth: 16px;\n\theight: 16px;\n\tborder: 2px solid rgba(0, 0, 0, 0.1);\n\tborder-radius: 50%;\n\tborder-top-color: var(--color-primary);\n\tanimation: spin 1s linear infinite;\n\tmargin-right: 10px;\n}\n\n@keyframes spin {\n\tto {\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n@keyframes fadeIn {\n\tfrom {\n\t\topacity: 0;\n\t}\n\tto {\n\t\topacity: 1;\n\t}\n}\n\n@keyframes slideIn {\n\tfrom {\n\t\ttransform: translateY(20px);\n\t\topacity: 0;\n\t}\n\tto {\n\t\ttransform: translateY(0);\n\t\topacity: 1;\n\t}\n}\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Performance Optimized Component\nclass OptimizedComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.shadow = this.attachShadow({ mode: &quot;open&quot; });\n\n    // Track timing\n    this.initialRenderTime = null;\n    this.fullRenderTime = null;\n\n    // Log the construction start time\n    this.constructorStartTime = performance.now();\n\n    // STEP 1: Critical Path Rendering - Minimal structural styles\n    this._createCriticalStructure();\n\n    // Record the time for critical path rendering\n    this.initialRenderTime = performance.now() - this.constructorStartTime;\n\n    // STEP 2: Defer non-critical styles\n    this._scheduleDeferredStyles();\n  }\n\n  connectedCallback() {\n    // Start the content loading simulation\n    this._simulateContentLoading();\n  }\n\n  _createCriticalStructure() {\n    // Create and apply only critical CSS for initial render\n    const criticalStyle = document.createElement(&quot;style&quot;);\n    criticalStyle.textContent = `\n          :host {\n            display: block;\n          }\n          \n          .card {\n            min-height: 300px;\n            border: 1px solid var(--color-border, #dee2e6);\n            border-radius: 8px;\n            overflow: hidden;\n          }\n          \n          .card-header {\n            padding: 16px;\n            background-color: rgba(0,0,0,0.03);\n            border-bottom: 1px solid var(--color-border, #dee2e6);\n          }\n          \n          .card-body {\n            padding: 16px;\n          }\n          \n          .card-title {\n            margin: 0;\n            font-size: 18px;\n          }\n          \n          .loading {\n            display: flex;\n            align-items: center;\n            font-size: 14px;\n            color: var(--color-text-secondary, #6c757d);\n          }\n        `;\n\n    // Create minimal structure with content placeholders\n    const card = document.createElement(&quot;div&quot;);\n    card.className = &quot;card&quot;;\n\n    const header = document.createElement(&quot;div&quot;);\n    header.className = &quot;card-header&quot;;\n\n    const title = document.createElement(&quot;h3&quot;);\n    title.className = &quot;card-title&quot;;\n    title.textContent = &quot;Optimized Component&quot;;\n\n    const body = document.createElement(&quot;div&quot;);\n    body.className = &quot;card-body&quot;;\n\n    const loading = document.createElement(&quot;div&quot;);\n    loading.className = &quot;loading&quot;;\n    loading.innerHTML =\n    &#39;&lt;div class=&quot;spinner&quot;&gt;&lt;/div&gt; Loading component content...&#39;;\n\n    // Assemble the structure\n    header.appendChild(title);\n    body.appendChild(loading);\n    card.appendChild(header);\n    card.appendChild(body);\n\n    // Add to shadow DOM\n    this.shadow.appendChild(criticalStyle);\n    this.shadow.appendChild(card);\n\n    // Store reference to body for content updates\n    this.cardBody = body;\n  }\n\n  _scheduleDeferredStyles() {\n    // Use requestIdleCallback (or fallback to setTimeout) for non-critical styling\n    const scheduleDeferred =\n    window.requestIdleCallback || (cb =&gt; setTimeout(cb, 200));\n\n    scheduleDeferred(() =&gt; {\n      this._applyDeferredStyles();\n    });\n  }\n\n  _applyDeferredStyles() {\n    // Create enhanced styling for the component\n    const enhancedStyle = document.createElement(&quot;style&quot;);\n    enhancedStyle.textContent = `\n          .card {\n            box-shadow: var(--shadow-md, 0 4px 6px rgba(0,0,0,0.1));\n            transition: transform 0.3s ease, box-shadow 0.3s ease;\n            background-color: var(--color-surface, #ffffff);\n          }\n          \n          .card:hover {\n            transform: translateY(-4px);\n            box-shadow: var(--shadow-lg, 0 10px 15px rgba(0,0,0,0.1));\n          }\n          \n          .card-header {\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            transition: background-color 0.3s ease;\n          }\n          \n          .card-title {\n            color: var(--color-primary, #0d6efd);\n            font-weight: 600;\n            transition: color 0.3s ease;\n          }\n          \n          .card-body {\n            color: var(--color-text, #212529);\n            transition: color 0.3s ease;\n          }\n          \n          .card-content {\n            animation: fadeIn 0.5s ease forwards;\n          }\n          \n          .feature-list {\n            padding: 0;\n            list-style-position: inside;\n          }\n          \n          .feature-item {\n            padding: 8px 0;\n            border-bottom: 1px solid var(--color-border, #dee2e6);\n            transition: background-color 0.3s ease;\n          }\n          \n          .feature-item:last-child {\n            border-bottom: none;\n          }\n          \n          .card-footer {\n            padding: 16px;\n            background-color: rgba(0,0,0,0.02);\n            border-top: 1px solid var(--color-border, #dee2e6);\n            text-align: center;\n          }\n          \n          .btn {\n            display: inline-block;\n            padding: 8px 16px;\n            background-color: var(--color-primary, #0d6efd);\n            color: white;\n            border-radius: 4px;\n            text-decoration: none;\n            transition: background-color 0.3s ease;\n          }\n          \n          .btn:hover {\n            background-color: #0b5ed7;\n          }\n          \n          .badge {\n            display: inline-block;\n            padding: 4px 8px;\n            background-color: rgba(13, 110, 253, 0.2);\n            color: var(--color-primary, #0d6efd);\n            border-radius: 20px;\n            font-size: 12px;\n          }\n          \n          @keyframes fadeIn {\n            from { opacity: 0; }\n            to { opacity: 1; }\n          }\n        `;\n\n    this.shadow.appendChild(enhancedStyle);\n  }\n\n  _simulateContentLoading() {\n    // Simulate content loading delay\n    setTimeout(() =&gt; {\n      // Replace loading indicator with actual content\n      const content = document.createElement(&quot;div&quot;);\n      content.className = &quot;card-content&quot;;\n\n      content.innerHTML = `\n            &lt;h4&gt;Performance Optimized Component&lt;/h4&gt;\n            &lt;p&gt;This component prioritizes the critical rendering path:&lt;/p&gt;\n            &lt;ul class=&quot;feature-list&quot;&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;Minimal initial CSS for faster painting&lt;/li&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;Deferred non-critical styles using requestIdleCallback&lt;/li&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;Progressive enhancement approach&lt;/li&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;Optimized animation and transition performance&lt;/li&gt;\n            &lt;/ul&gt;\n            &lt;div class=&quot;card-footer&quot;&gt;\n              &lt;span class=&quot;badge&quot;&gt;Optimized&lt;/span&gt;\n              &lt;a href=&quot;#&quot; class=&quot;btn&quot;&gt;Learn More&lt;/a&gt;\n            &lt;/div&gt;\n          `;\n\n      // Replace loading state with content\n      this.cardBody.innerHTML = &quot;&quot;;\n      this.cardBody.appendChild(content);\n\n      // Record the time for full rendering\n      this.fullRenderTime = performance.now() - this.constructorStartTime;\n\n      // Dispatch event when component is fully rendered\n      this.dispatchEvent(\n      new CustomEvent(&quot;component-rendered&quot;, {\n        bubbles: true,\n        composed: true,\n        detail: {\n          type: &quot;optimized&quot;,\n          initialRender: this.initialRenderTime,\n          fullRender: this.fullRenderTime } }));\n\n\n\n    }, 800); // Simulate a network delay of 800ms\n  }}\n\n\n// Unoptimized Component (monolithic approach, loads everything at once)\nclass UnoptimizedComponent extends HTMLElement {\n  constructor() {\n    super();\n    this.shadow = this.attachShadow({ mode: &quot;open&quot; });\n\n    // Track timing\n    this.constructorStartTime = performance.now();\n\n    // Apply all styles and structure immediately (unoptimized approach)\n    this._createFullComponent();\n  }\n\n  connectedCallback() {\n    // Start the content loading simulation\n    this._simulateContentLoading();\n  }\n\n  _createFullComponent() {\n    // Create ALL styles upfront - this blocks the initial render\n    const style = document.createElement(&quot;style&quot;);\n    style.textContent = `\n          :host {\n            display: block;\n          }\n          \n          .card {\n            min-height: 300px;\n            border: 1px solid var(--color-border, #dee2e6);\n            border-radius: 8px;\n            overflow: hidden;\n            box-shadow: var(--shadow-md, 0 4px 6px rgba(0,0,0,0.1));\n            transition: transform 0.3s ease, box-shadow 0.3s ease;\n            background-color: var(--color-surface, #ffffff);\n          }\n          \n          .card:hover {\n            transform: translateY(-4px);\n            box-shadow: var(--shadow-lg, 0 10px 15px rgba(0,0,0,0.1));\n          }\n          \n          .card-header {\n            padding: 16px;\n            background-color: rgba(0,0,0,0.03);\n            border-bottom: 1px solid var(--color-border, #dee2e6);\n            display: flex;\n            justify-content: space-between;\n            align-items: center;\n            transition: background-color 0.3s ease;\n          }\n          \n          .card-title {\n            margin: 0;\n            font-size: 18px;\n            color: var(--color-primary, #0d6efd);\n            font-weight: 600;\n            transition: color 0.3s ease;\n          }\n          \n          .card-body {\n            padding: 16px;\n            color: var(--color-text, #212529);\n            transition: color 0.3s ease;\n          }\n          \n          .loading {\n            display: flex;\n            align-items: center;\n            font-size: 14px;\n            color: var(--color-text-secondary, #6c757d);\n          }\n          \n          .card-content {\n            animation: fadeIn 0.5s ease forwards;\n          }\n          \n          .feature-list {\n            padding: 0;\n            list-style-position: inside;\n          }\n          \n          .feature-item {\n            padding: 8px 0;\n            border-bottom: 1px solid var(--color-border, #dee2e6);\n            transition: background-color 0.3s ease;\n          }\n          \n          .feature-item:last-child {\n            border-bottom: none;\n          }\n          \n          .card-footer {\n            padding: 16px;\n            background-color: rgba(0,0,0,0.02);\n            border-top: 1px solid var(--color-border, #dee2e6);\n            text-align: center;\n          }\n          \n          .btn {\n            display: inline-block;\n            padding: 8px 16px;\n            background-color: var(--color-primary, #0d6efd);\n            color: white;\n            border-radius: 4px;\n            text-decoration: none;\n            transition: background-color 0.3s ease;\n          }\n          \n          .btn:hover {\n            background-color: #0b5ed7;\n          }\n          \n          .badge {\n            display: inline-block;\n            padding: 4px 8px;\n            background-color: rgba(13, 110, 253, 0.2);\n            color: var(--color-primary, #0d6efd);\n            border-radius: 20px;\n            font-size: 12px;\n          }\n          \n          @keyframes fadeIn {\n            from { opacity: 0; }\n            to { opacity: 1; }\n          }\n          \n          /* Extra unnecessary styles to simulate bloat */\n          .unused-1 { color: red; }\n          .unused-2 { background-color: green; }\n          .unused-3 { border: 1px solid blue; }\n          .unused-4 { margin: 10px; }\n          .unused-5 { padding: 15px; }\n          .unused-6 { font-size: 24px; }\n          .unused-7 { text-align: center; }\n          .unused-8 { display: flex; }\n          .unused-9 { position: relative; }\n          .unused-10 { width: 100%; }\n        `;\n\n    // Create the full structure\n    const card = document.createElement(&quot;div&quot;);\n    card.className = &quot;card&quot;;\n\n    const header = document.createElement(&quot;div&quot;);\n    header.className = &quot;card-header&quot;;\n\n    const title = document.createElement(&quot;h3&quot;);\n    title.className = &quot;card-title&quot;;\n    title.textContent = &quot;Unoptimized Component&quot;;\n\n    const body = document.createElement(&quot;div&quot;);\n    body.className = &quot;card-body&quot;;\n\n    const loading = document.createElement(&quot;div&quot;);\n    loading.className = &quot;loading&quot;;\n    loading.innerHTML =\n    &#39;&lt;div class=&quot;spinner&quot;&gt;&lt;/div&gt; Loading component content...&#39;;\n\n    // Assemble the structure\n    header.appendChild(title);\n    body.appendChild(loading);\n    card.appendChild(header);\n    card.appendChild(body);\n\n    // Add to shadow DOM (all at once, blocking rendering)\n    this.shadow.appendChild(style);\n    this.shadow.appendChild(card);\n\n    // Store reference to body for content updates\n    this.cardBody = body;\n\n    // Simulate parsing delay with some dummy operations\n    this._simulateHeavyParsing();\n\n    // Record initial render time (which is already delayed due to heavy parsing)\n    this.initialRenderTime = performance.now() - this.constructorStartTime;\n  }\n\n  _simulateHeavyParsing() {\n    // Simulate intensive parsing of a large component\n    const dummyArray = [];\n    for (let i = 0; i &lt; 10000; i++) {\n      dummyArray.push({ id: i, value: `item-${i}` });\n    }\n\n    // Sort the array to simulate processing\ndummyArray.sort((a, b) =&gt; b.id - a.id);\n\n    // More dummy operations\n    let result = 0;\n    for (let i = 0; i &lt; 5000; i++) {\n      result += Math.sin(i) * Math.cos(i);\n    }\n  }\n\n  _simulateContentLoading() {\n    // Simulate content loading delay (longer than optimized)\n    setTimeout(() =&gt; {\n      // Replace loading indicator with actual content\n      const content = document.createElement(&quot;div&quot;);\n      content.className = &quot;card-content&quot;;\n\n      content.innerHTML = `\n            &lt;h4&gt;Unoptimized Component&lt;/h4&gt;\n            &lt;p&gt;This component loads everything at once:&lt;/p&gt;\n            &lt;ul class=&quot;feature-list&quot;&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;All CSS loaded upfront, blocking render&lt;/li&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;No prioritization of critical styles&lt;/li&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;Contains unused CSS that must be parsed&lt;/li&gt;\n              &lt;li class=&quot;feature-item&quot;&gt;No deferred loading strategy&lt;/li&gt;\n            &lt;/ul&gt;\n            &lt;div class=&quot;card-footer&quot;&gt;\n              &lt;span class=&quot;badge&quot;&gt;Unoptimized&lt;/span&gt;\n              &lt;a href=&quot;#&quot; class=&quot;btn&quot;&gt;Learn More&lt;/a&gt;\n            &lt;/div&gt;\n          `;\n\n      // Replace loading state with content\n      this.cardBody.innerHTML = &quot;&quot;;\n      this.cardBody.appendChild(content);\n\n      // Record the time for full rendering\n      this.fullRenderTime = performance.now() - this.constructorStartTime;\n\n      // Dispatch event when component is fully rendered\n      this.dispatchEvent(\n      new CustomEvent(&quot;component-rendered&quot;, {\n        bubbles: true,\n        composed: true,\n        detail: {\n          type: &quot;unoptimized&quot;,\n          initialRender: this.initialRenderTime,\n          fullRender: this.fullRenderTime } }));\n\n\n\n    }, 1200); // Simulate a longer network delay of 1200ms\n  }}\n\n\n// Register components\ncustomElements.define(&quot;optimized-component&quot;, OptimizedComponent);\ncustomElements.define(&quot;unoptimized-component&quot;, UnoptimizedComponent);\n\n// Demo Controller\nclass DemoController {\n  constructor() {\n    this.optimizedContainer = document.getElementById(&quot;optimized-container&quot;);\n    this.unoptimizedContainer = document.getElementById(&quot;unoptimized-container&quot;);\n    this.timelineTrack = document.getElementById(&quot;timeline-track&quot;);\n\n    this.criticalTimingElement = document.getElementById(&quot;critical-timing&quot;);\n    this.optimizedTimingElement = document.getElementById(&quot;optimized-timing&quot;);\n    this.unoptimizedTimingElement = document.getElementById(&quot;unoptimized-timing&quot;);\n\n    this.startTime = null;\n    this.events = [];\n\n    this.setupEventListeners();\n  }\n\n  setupEventListeners() {\n    document.\n    getElementById(&quot;load-both&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.loadComponents());\n    document.\n    getElementById(&quot;reset-demo&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.resetDemo());\n    document.\n    getElementById(&quot;toggle-theme&quot;).\n    addEventListener(&quot;click&quot;, () =&gt; this.toggleTheme());\n\n    // Listen for component render events\n    document.addEventListener(&quot;component-rendered&quot;, e =&gt; {\n      this.handleComponentRendered(e);\n    });\n  }\n\n  loadComponents() {\n    // Reset the demo first\n    this.resetDemo();\n\n    // Record start time\n    this.startTime = performance.now();\n\n    // Add a &quot;start&quot; event to the timeline\n    this.addTimelineEvent(&quot;Demo Started&quot;, 0, &quot;#198754&quot;);\n\n    // Create and add the components\n    const optimizedComponent = document.createElement(&quot;optimized-component&quot;);\n    const unoptimizedComponent = document.createElement(&quot;unoptimized-component&quot;);\n\n    this.optimizedContainer.innerHTML = &quot;&quot;;\n    this.unoptimizedContainer.innerHTML = &quot;&quot;;\n\n    this.optimizedContainer.appendChild(optimizedComponent);\n    this.unoptimizedContainer.appendChild(unoptimizedComponent);\n\n    // Disable the load button\n    document.getElementById(&quot;load-both&quot;).disabled = true;\n  }\n\n  handleComponentRendered(event) {\n    const { type, initialRender, fullRender } = event.detail;\n    const elapsedTime = performance.now() - this.startTime;\n\n    if (type === &quot;optimized&quot;) {\n      // Add timeline events for optimized component\n      this.addTimelineEvent(&quot;Optimized Critical Render&quot;, initialRender, &quot;#0d6efd&quot;);\n      this.addTimelineEvent(&quot;Optimized Full Render&quot;, fullRender, &quot;#0d6efd&quot;);\n\n      // Update timing display\n      this.criticalTimingElement.textContent = `${initialRender.toFixed(1)}ms`;\n      this.optimizedTimingElement.textContent = `${fullRender.toFixed(1)}ms`;\n    } else if (type === &quot;unoptimized&quot;) {\n      // Add timeline events for unoptimized component\n      this.addTimelineEvent(\n      &quot;Unoptimized Initial Render&quot;,\n      initialRender,\n      &quot;#dc3545&quot;);\n\n      this.addTimelineEvent(&quot;Unoptimized Full Render&quot;, fullRender, &quot;#dc3545&quot;);\n\n      // Update timing display\n      this.unoptimizedTimingElement.textContent = `${fullRender.toFixed(1)}ms`;\n\n      // Re-enable the load button\n      setTimeout(() =&gt; {\n        document.getElementById(&quot;load-both&quot;).disabled = false;\n      }, 500);\n    }\n  }\n\n  addTimelineEvent(name, time, color) {\n    // Calculate position on timeline (scaled to 2000ms max)\n    const position = Math.min(time / 2000, 1) * 100;\n\n    // Create marker element\n    const marker = document.createElement(&quot;div&quot;);\n    marker.className = &quot;timeline-marker&quot;;\n    marker.style.left = `${position}%`;\n    marker.style.borderLeftColor = color;\n    marker.style.color = color;\n    marker.textContent = `${name}: ${time.toFixed(1)}ms`;\n\n    // Add to timeline\n    this.timelineTrack.appendChild(marker);\n\n    // Store event\n    this.events.push({\n      name,\n      time,\n      element: marker });\n\n  }\n\n  resetDemo() {\n    // Clear containers\n    this.optimizedContainer.innerHTML =\n    &#39;&lt;p&gt;&lt;i&gt;Click &quot;Load Both Components&quot; to begin the demo...&lt;/i&gt;&lt;/p&gt;&#39;;\n    this.unoptimizedContainer.innerHTML =\n    &#39;&lt;p&gt;&lt;i&gt;Click &quot;Load Both Components&quot; to begin the demo...&lt;/i&gt;&lt;/p&gt;&#39;;\n\n    // Clear timeline\n    this.timelineTrack.innerHTML = &quot;&quot;;\n\n    // Reset timing display\n    this.criticalTimingElement.textContent = &quot;-&quot;;\n    this.optimizedTimingElement.textContent = &quot;-&quot;;\n    this.unoptimizedTimingElement.textContent = &quot;-&quot;;\n\n    // Clear events\n    this.events = [];\n\n    // Re-enable the load button\n    document.getElementById(&quot;load-both&quot;).disabled = false;\n  }\n\n  toggleTheme() {\n    const root = document.documentElement;\n    const computedStyle = getComputedStyle(root);\n    const currentBg = computedStyle.getPropertyValue(&quot;--color-bg&quot;).trim();\n\n    // Check if we&#39;re in dark mode\n    const isDark = currentBg === &quot;#212529&quot; || currentBg === &quot;rgb(33, 37, 41)&quot;;\n\n    if (isDark) {\n      // Switch to light mode\n      root.style.setProperty(&quot;--color-bg&quot;, &quot;#f8f9fa&quot;);\n      root.style.setProperty(&quot;--color-surface&quot;, &quot;#ffffff&quot;);\n      root.style.setProperty(&quot;--color-text&quot;, &quot;#212529&quot;);\n      root.style.setProperty(&quot;--color-text-secondary&quot;, &quot;#6c757d&quot;);\n      root.style.setProperty(&quot;--color-border&quot;, &quot;#dee2e6&quot;);\n    } else {\n      // Switch to dark mode\n      root.style.setProperty(&quot;--color-bg&quot;, &quot;#212529&quot;);\n      root.style.setProperty(&quot;--color-surface&quot;, &quot;#343a40&quot;);\n      root.style.setProperty(&quot;--color-text&quot;, &quot;#f8f9fa&quot;);\n      root.style.setProperty(&quot;--color-text-secondary&quot;, &quot;#adb5bd&quot;);\n      root.style.setProperty(&quot;--color-border&quot;, &quot;#495057&quot;);\n    }\n  }}\n\n\n// Initialize the demo when the DOM is ready\ndocument.addEventListener(&quot;DOMContentLoaded&quot;, () =&gt; {\n  new DemoController();\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\">the original article</a>.</p></figure>\n<h2 id=\"integration-with-design-systems-and-component-libraries\">Integration with Design Systems and Component Libraries<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#integration-with-design-systems-and-component-libraries\" aria-label=\"Link to this section\">#</a></h2>\n<p>Advanced Web Component styling often requires integration with existing design systems. This approach demonstrates a technique for adapting to popular frameworks:</p>\n<pre><code class=\"language-javascript\">class AdaptiveComponent extends HTMLElement {\n  static get observedAttributes() {\n    return ['design-system'];\n  }\n\n  constructor() {\n    super();\n    this.shadow = this.attachShadow({ mode: 'open' });\n\n    // Base structure\n    this.shadow.innerHTML = `\n      &lt;div class=\"adaptive-container\"&gt;\n        &lt;slot&gt;&lt;/slot&gt;\n      &lt;/div&gt;\n    `;\n\n    // Initialize with default styling\n    this._applyDesignSystem();\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (name === 'design-system' &amp;&amp; oldValue !== newValue) {\n      this._applyDesignSystem();\n    }\n  }\n\n  _applyDesignSystem() {\n    const system = this.getAttribute('design-system') || 'default';\n\n    // Remove previous design system styles\n    const previousStyle = this.shadow.querySelector('.design-system-styles');\n    if (previousStyle) {\n      previousStyle.remove();\n    }\n\n    // Apply appropriate design system mapping\n    const styleElement = document.createElement('style');\n    styleElement.className = 'design-system-styles';\n\n    // Design system adaptations\n    const designSystemMappings = {\n      'default': `\n        .adaptive-container {\n          font-family: system-ui, sans-serif;\n          background-color: #ffffff;\n          padding: 16px;\n          border-radius: 4px;\n          box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n      `,\n      'material': `\n        .adaptive-container {\n          font-family: Roboto, sans-serif;\n          background-color: #ffffff;\n          padding: 16px;\n          border-radius: 4px;\n          box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);\n        }\n      `,\n      'tailwind': `\n        .adaptive-container {\n          font-family: Inter, system-ui, sans-serif;\n          background-color: #ffffff;\n          padding: 1rem;\n          border-radius: 0.25rem;\n          box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n        }\n      `\n    };\n\n    styleElement.textContent = designSystemMappings[system] || designSystemMappings.default;\n    this.shadow.prepend(styleElement);\n  }\n}\n\ncustomElements.define('adaptive-component', AdaptiveComponent);\n</code></pre>\n<p>This implementation showcases:</p>\n<ul>\n<li>\n<p>Adaptive styling based on design system context</p>\n</li>\n<li>\n<p>Dynamic style switching without component reconstruction</p>\n</li>\n<li>\n<p>Preservation of component functionality across styling changes</p>\n</li>\n</ul>\n<h2 id=\"best-practices-for-production-grade-web-component-styling\">Best Practices for Production-Grade Web Component Styling<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#best-practices-for-production-grade-web-component-styling\" aria-label=\"Link to this section\">#</a></h2>\n<h3 id=\"1-optimize-for-render-performance\">1. Optimize for Render Performance<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#1-optimize-for-render-performance\" aria-label=\"Link to this section\">#</a></h3>\n<ul>\n<li>\n<p>Use CSS containment to isolate rendering paths: <code>contain: content;</code></p>\n</li>\n<li>\n<p>Apply critical CSS first, defer non-critical styles</p>\n</li>\n<li>\n<p>Minimize style recalculations by batching DOM operations</p>\n</li>\n<li>\n<p>Use constructable stylesheets for shared styles</p>\n</li>\n<li>\n<p>Avoid deep DOM trees within the shadow root</p>\n</li>\n</ul>\n<h3 id=\"2-design-a-clear-styling-api\">2. Design a Clear Styling API<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#2-design-a-clear-styling-api\" aria-label=\"Link to this section\">#</a></h3>\n<ul>\n<li>\n<p>Create a consistent naming convention for CSS custom properties</p>\n</li>\n<li>\n<p>Document all customization points in component documentation</p>\n</li>\n<li>\n<p>Provide sensible defaults for all custom properties</p>\n</li>\n<li>\n<p>Use logical property groups (spacing, colors, typography)</p>\n</li>\n<li>\n<p>Consider component variants as entry points for design system integration</p>\n</li>\n</ul>\n<h3 id=\"3-test-across-styling-contexts\">3. Test Across Styling Contexts<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#3-test-across-styling-contexts\" aria-label=\"Link to this section\">#</a></h3>\n<ul>\n<li>\n<p>Verify component appearance in light and dark themes</p>\n</li>\n<li>\n<p>Test inherited properties like font-family and text color</p>\n</li>\n<li>\n<p>Ensure responsive behavior across viewports</p>\n</li>\n<li>\n<p>Validate that CSS custom properties fall back gracefully</p>\n</li>\n<li>\n<p>Verify that component styling doesn't break when nested</p>\n</li>\n</ul>\n<h3 id=\"4-use-preprocessing-for-development-experience\">4. Use Preprocessing for Development Experience<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#4-use-preprocessing-for-development-experience\" aria-label=\"Link to this section\">#</a></h3>\n<ul>\n<li>\n<p>Consider using SASS/LESS for maintainable component styles</p>\n</li>\n<li>\n<p>Build time CSS custom property optimization</p>\n</li>\n<li>\n<p>Generate documentation from component style definitions</p>\n</li>\n<li>\n<p>Integrate linting tools for consistent styling patterns</p>\n</li>\n<li>\n<p>Implement style regression testing for component libraries</p>\n</li>\n</ul>\n<h2 id=\"the-intersection-of-styling-and-component-lifecycle\">The Intersection of Styling and Component Lifecycle<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#the-intersection-of-styling-and-component-lifecycle\" aria-label=\"Link to this section\">#</a></h2>\n<p>Throughout this article, we've explored sophisticated styling techniques for Web Components, but it's important to recognize that styling doesn't exist in isolation. The component's lifecycle has profound implications for when and how styles are applied, updated, and removed.</p>\n<p>Key lifecycle-related styling considerations include:</p>\n<ol>\n<li>\n<p><strong>Style Initialization Timing</strong>: Styles defined in the constructor are applied before the component is connected to the DOM, which can impact initial rendering performance.</p>\n</li>\n<li>\n<p><strong>Dynamic Style Updates</strong>: Many components need to update their styles in response to attribute changes, props, or state changes—operations that typically occur during lifecycle events.</p>\n</li>\n<li>\n<p><strong>Style Cleanup</strong>: When components are removed from the DOM, any associated stylesheets or external resources should be properly cleaned up to prevent memory leaks.</p>\n</li>\n</ol>\n<p>Here's how component lifecycle methods relate to styling. In one line each: <code>connectedCallback</code> runs when the element is added to the page, <code>disconnectedCallback</code> when it is removed, and <code>attributeChangedCallback</code> when a watched attribute changes (<a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">the lifecycle post</a> covers all three in depth):</p>\n<pre><code class=\"language-javascript\">class LifecycleAwareStyledComponent extends HTMLElement {\n  static get observedAttributes() {\n    return ['theme', 'size', 'disabled'];\n  }\n\n  constructor() {\n    super();\n    this.shadow = this.attachShadow({ mode: 'open' });\n\n    // Initial styles applied during construction\n    this._createBaseStyles();\n  }\n\n  connectedCallback() {\n    // Apply context-dependent styles when added to DOM\n    this._applyContextualStyles();\n\n    // Initialize theme from current document context\n    this._synchronizeWithGlobalTheme();\n\n    // Add event listeners for theme changes\n    document.addEventListener('theme-changed', this._handleThemeChange);\n  }\n\n  attributeChangedCallback(name, oldValue, newValue) {\n    if (oldValue === newValue) return;\n\n    // Update styles based on attribute changes\n    switch(name) {\n      case 'theme':\n        this._updateThemeStyles(newValue);\n        break;\n      case 'size':\n        this._updateSizeStyles(newValue);\n        break;\n      case 'disabled':\n        this._updateDisabledState(newValue !== null);\n        break;\n    }\n  }\n\n  disconnectedCallback() {\n    // Clean up any theme listeners\n    document.removeEventListener('theme-changed', this._handleThemeChange);\n\n    // Remove any dynamically added stylesheets\n    // (important for preventing memory leaks)\n    if (this._adoptedStyleSheet) {\n      this.shadow.adoptedStyleSheets = \n        this.shadow.adoptedStyleSheets.filter(sheet =&gt; sheet !== this._adoptedStyleSheet);\n    }\n  }\n\n  // Component-specific implementation of style methods...\n}\n</code></pre>\n<p>Styling and lifecycle are tied together, so it helps to understand both. The lifecycle methods and component communication are covered in <a href=\"https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/\">their own post</a>.</p>\n<h2 id=\"conclusion-building-a-cohesive-component-ecosystem\">Conclusion: Building a Cohesive Component Ecosystem<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#conclusion-building-a-cohesive-component-ecosystem\" aria-label=\"Link to this section\">#</a></h2>\n<p>Page CSS stops at the shadow boundary, and you choose the doors: <code>:host</code> for the element itself, <code>::slotted</code> for content the page supplies, custom properties for values the component reads, and <code>::part</code> for named inner pieces. Everything else here (constructable stylesheets, containment, design tokens) is a way to do those four things at scale.</p>\n<p>The demo project for this post is on GitHub if you want the full source: <a href=\"https://github.com/mnichols08/WebComponentStyles\">Demonstration GitHub Repository</a> and <a href=\"https://mnichols08.github.io/WebComponentStyles/\">Demonstration Deployment</a>.</p>\n<p>Styling decides how a component looks. The next question is whether everyone can use it: with a keyboard, with a screen reader, with focus where they expect it.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "Mastering the Art of Web Component Styling\nCanonical: https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/\nAdvanced Techniques for Modern Front-End Architecture\nYou drop a custom component into a page and the page's CSS turns it into a mess. Or you want to restyle a component from outside and nothing you write seems to reach it. Both come from the same rule: a component's inside is walled off from the page's CSS, and you have to decide where to cut doors in the wall.\nThis post shows the four doors that let the page adjust a component on purpose: :host, ::slotted, CSS custom properties and ::part. The sections after \"Inheritance and Context Adaptation\" (performance, design-system integration, lifecycle) are advanced extras you can skim.\nA two-line recap if you are new here: a custom element is a class that extends HTMLElement, and attachShadow gives it a private shadow tree whose inner elements page CSS cannot select.\nShadow DOM: The Technical Foundation of Style Encapsulation\nThe Shadow DOM provides a powerful isolation mechanism that creates a separate DOM tree with its own scoped styling context. This architectural approach solves one of the most persistent challenges in front-end development: CSS collision and specificity conflicts.\nWhen implementing Shadow DOM encapsulation, you're effectively creating a styling boundary with controlled entry and exit points:\nclass AdvancedComponent extends HTMLElement {\nconstructor() {\nsuper();\n// Create an isolated DOM tree with its own styling context\nconst shadow = this.attachShadow({\nmode: 'open',\n// Optional: focus behavior, not styling. Clicking non-focusable\n// parts of the shadow tree focuses its first focusable element,\n// and focusing the host delegates focus to that element.\ndelegatesFocus: true\n});\nshadow.innerHTML = `\n<style>\n:host {\ndisplay: block;\ncontain: content; /* CSS containment for performance */\n}\n.container {\nbackground-color: var(--bg-primary, #f0f4f8);\npadding: var(--spacing-md, 16px);\nborder-radius: var(--border-radius-md, 4px);\nbox-shadow: var(--shadow-sm, 0 1px 3px rgba(0,0,0,0.1));\ntransition: all 180ms ease-out;\n}\n::slotted(*) {\nmargin-bottom: var(--spacing-sm, 8px);\n}\n</style>\n<div class=\"container\">\n<slot></slot>\n</div>\n`;\n}\n}\ncustomElements.define('advanced-component', AdvancedComponent);\nThis implementation showcases several patterns (note that delegatesFocus has nothing to do with styles; it only changes how focus moves into the component):\nThe :host selector targets the custom element itself\nCSS containment optimizes rendering performance\nCSS custom properties create a styling API\nThe ::slotted pseudo-element styles projected content\nSeeing is believing when it comes to understanding the true power of Shadow DOM encapsulation. In this interactive example, you can observe how Shadow DOM completely isolates component styles from the surrounding page. Notice how the global styles (with red borders and yellow backgrounds) affect the regular DOM element but have no effect on the Shadow DOM components. Try toggling between light and dark mode to see how components adapt, and inspect the difference between open and closed Shadow DOM modes.\nHTML\n<h2>Shadow DOM Style Encapsulation</h2>\n<p>\nThis demo shows how Shadow DOM protects components from external styles.\nThe global styles with red borders and yellow backgrounds have no effect on the Shadow DOM component.\n</p>\n<div class=\"demo-container\">\n<h3>Regular DOM (No Shadow DOM)</h3>\n<div class=\"global-card card\">\n<h4>Regular DOM Card</h4>\n<p>This card uses regular DOM and is affected by global styles.</p>\n</div>\n<h3>Shadow DOM (Open Mode)</h3>\n<shadow-card mode=\"open\"></shadow-card>\n<h3>Shadow DOM (Closed Mode)</h3>\n<shadow-card mode=\"closed\"></shadow-card>\n</div>\n<div class=\"button-row\">\n<button id=\"inspect-open\">Inspect Open Shadow DOM</button>\n<button id=\"inspect-closed\">Inspect Closed Shadow DOM</button>\n<button id=\"toggle-theme\">Toggle Light/Dark Mode</button>\n</div>\n<div id=\"console-output\" style=\"border: 1px solid var(--border-color); padding: 10px; border-radius: 4px; margin-top: 20px; background-color: var(--card-bg);\">\n<p>Console output will appear here...</p>\n</div>\nCSS\n/* Light/Dark mode variables */\n:root {\n--bg-color: #f8f9fa;\n--text-color: #333;\n--border-color: #dee2e6;\n--card-bg: #fff;\n--button-primary: #0d6efd;\n--button-text: #fff;\ncolor-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #212529;\n--text-color: #f8f9fa;\n--border-color: #495057;\n--card-bg: #343a40;\n--button-primary: #0d6efd;\n--button-text: #fff;\n}\n}\nbody {\nfont-family: system-ui, -apple-system, sans-serif;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\ntransition: background-color 0.3s ease;\n}\nh2 {\nmargin-top: 0;\n}\n.demo-container {\nmargin: 20px 0;\n}\n/* Global styles that won't affect shadow DOM */\n.global-card {\nborder: 2px solid red;\npadding: 20px;\nborder-radius: 8px;\nbackground-color: yellow;\ncolor: purple;\n}\np {\ncolor: var(--text-color);\n}\n.button-row {\ndisplay: flex;\ngap: 10px;\nmargin: 20px 0;\n}\nbutton {\npadding: 8px 16px;\nbackground-color: var(--button-primary);\ncolor: var(--button-text);\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\n/* This style tries to target all cards, but won't affect Shadow DOM */\n.card {\nborder: 2px solid red !important;\nbackground-color: yellow !important;\ncolor: purple !important;\n}\nJavaScript\nclass ShadowCard extends HTMLElement {\nconstructor() {\nsuper();\nconst mode = this.getAttribute(\"mode\") || \"open\";\nthis.shadow = this.attachShadow({ mode });\nthis.render();\n}\nrender() {\nconst style = document.createElement(\"style\");\nstyle.textContent = `\n:host {\ndisplay: block;\nmargin: 20px 0;\n}\n.card {\nbackground-color: var(--card-bg, #fff);\nborder: 1px solid var(--border-color, #dee2e6);\nborder-radius: 8px;\npadding: 20px;\ncolor: var(--text-color, #333);\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh4 {\nmargin-top: 0;\ncolor: var(--text-color, #333);\n}\n`;\nconst card = document.createElement(\"div\");\ncard.className = \"card\";\nconst heading = document.createElement(\"h4\");\nheading.textContent = `Shadow DOM Card (${this.getAttribute(\"mode\")} mode)`;\nconst paragraph = document.createElement(\"p\");\nparagraph.textContent = `This card uses Shadow DOM in ${this.getAttribute(\n\"mode\")\n} mode and is protected from global styles.`;\ncard.appendChild(heading);\ncard.appendChild(paragraph);\nthis.shadow.appendChild(style);\nthis.shadow.appendChild(card);\n}}\ncustomElements.define(\"shadow-card\", ShadowCard);\n// UI functionality\ndocument.getElementById(\"inspect-open\").addEventListener(\"click\", () => {\nconst openCard = document.querySelector('shadow-card[mode=\"open\"]');\nconst log = document.getElementById(\"console-output\");\ntry {\nconst shadowRoot = openCard.shadowRoot;\nlog.innerHTML = \"<p>Open Shadow DOM successfully accessed! ✓</p>\";\n} catch (error) {\nlog.innerHTML =\n\"<p>Error accessing Open Shadow DOM: \" + error.message + \"</p>\";\n}\n});\ndocument.getElementById(\"inspect-closed\").addEventListener(\"click\", () => {\nconst closedCard = document.querySelector('shadow-card[mode=\"closed\"]');\nconst log = document.getElementById(\"console-output\");\ntry {\nconst shadowRoot = closedCard.shadowRoot;\nif (shadowRoot) {\nlog.innerHTML =\n\"<p>Closed Shadow DOM accessed! (This should not happen)</p>\";\n} else {\nlog.innerHTML =\n\"<p>Closed Shadow DOM access returned null. ✓ This is expected behavior.</p>\";\n}\n} catch (error) {\nlog.innerHTML =\n\"<p>Error accessing Closed Shadow DOM: \" + error.message + \"</p>\";\n}\n});\ndocument.getElementById(\"toggle-theme\").addEventListener(\"click\", () => {\nconst log = document.getElementById(\"console-output\");\n// Check current background color to determine current theme\nconst isDark = getComputedStyle(document.body).backgroundColor.match(\n/^rgb\\(33,\\s*37,\\s*41\\)$/);\nif (isDark) {\ndocument.documentElement.style.setProperty(\"--bg-color\", \"#f8f9fa\");\ndocument.documentElement.style.setProperty(\"--text-color\", \"#333\");\ndocument.documentElement.style.setProperty(\"--border-color\", \"#dee2e6\");\ndocument.documentElement.style.setProperty(\"--card-bg\", \"#fff\");\nlog.innerHTML = \"<p>Theme changed to light mode</p>\";\n} else {\ndocument.documentElement.style.setProperty(\"--bg-color\", \"#212529\");\ndocument.documentElement.style.setProperty(\"--text-color\", \"#f8f9fa\");\ndocument.documentElement.style.setProperty(\"--border-color\", \"#495057\");\ndocument.documentElement.style.setProperty(\"--card-bg\", \"#343a40\");\nlog.innerHTML = \"<p>Theme changed to dark mode</p>\";\n}\n});\nShadow DOM Style Encapsulation Demo on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\nUnderstanding Shadow DOM Modes: Open vs. Closed\nWhen attaching a shadow root to a component, the mode property defines a critical aspect of your component's encapsulation model. This choice directly impacts both styling and JavaScript interaction capabilities:\n// Open mode: External JavaScript can access the shadow DOM\nconst openShadow = element.attachShadow({ mode: 'open' });\n// Closed mode: External JavaScript cannot directly access the shadow DOM\nconst closedShadow = element.attachShadow({ mode: 'closed' });\nWhen to Use mode: 'closed'\nRecap of the Shadow DOM post: open lets page scripts reach the shadow tree through element.shadowRoot; closed makes that property return null. It matters little for styling, so feel free to skip ahead to \"Advanced Styling Architectures\".\nThe closed mode creates a stricter boundary around your component, preventing external JavaScript from accessing its shadow DOM through the .shadowRoot property. While open mode is more common for most use cases, closed mode serves specific technical purposes:\nSecurity-Sensitive Components: When building components that process sensitive data (payment forms, authentication interfaces)\nPreventing DOM Manipulation: To stop external scripts from modifying your component's internal structure\nStrict API Enforcement: Ensuring that all interactions happen through your explicitly defined APIs\nThird-Party Protection: Protecting your component from interference by third-party scripts on the page\nBrowser-Like Components: When emulating native browser elements that use closed shadow roots\nOne correction before the example: closed is a guard rail, not a security boundary. Other scripts on the page can still change the DOM around your component, and a script that runs before yours can patch attachShadow to keep a reference to the root. Do not rely on it to protect sensitive data.\nHere's an implementation example of a component with a closed shadow root:\nclass SecureComponent extends HTMLElement {\nconstructor() {\nsuper();\n// Create a closed shadow root\nconst shadow = this.attachShadow({ mode: 'closed' });\n// Store internal state and references privately using closure\nconst state = {\nvalue: '',\nisValid: false\n};\n// Create internal DOM structure\nconst input = document.createElement('input');\ninput.type = 'password';\ninput.setAttribute('placeholder', 'Enter secure data');\nconst statusIndicator = document.createElement('div');\nstatusIndicator.className = 'status-indicator';\n// Add styles with maximum encapsulation\nconst style = document.createElement('style');\nstyle.textContent = `\n:host {\ndisplay: block;\nborder: 1px solid #ddd;\nborder-radius: 4px;\npadding: 16px;\nbackground: #f9f9f9;\n}\ninput {\nborder: 1px solid #ccc;\npadding: 8px;\nborder-radius: 4px;\nwidth: 100%;\nbox-sizing: border-box;\n}\n.status-indicator {\nheight: 8px;\nmargin-top: 8px;\nborder-radius: 4px;\nbackground-color: #ccc;\ntransition: background-color 0.3s ease;\n}\n.status-indicator.valid {\nbackground-color: #4caf50;\n}\n.status-indicator.invalid {\nbackground-color: #f44336;\n}\n`;\n// Add event listeners that maintain encapsulation\ninput.addEventListener('input', (e) => {\nstate.value = e.target.value;\nthis._validateAndUpdate(state, statusIndicator);\n// Communicate changes through events rather than direct access\nthis.dispatchEvent(new CustomEvent('secure-input-change', {\nbubbles: true,\ncomposed: true,\ndetail: { isValid: state.isValid }\n}));\n});\n// Add elements to shadow DOM\nshadow.appendChild(style);\nshadow.appendChild(input);\nshadow.appendChild(statusIndicator);\n// Expose methods through the element itself, not through shadowRoot\nthis.reset = () => {\nstate.value = '';\nstate.isValid = false;\ninput.value = '';\nstatusIndicator.className = 'status-indicator';\n};\nthis.getValue = () => state.isValid ? state.value : null;\n}\n// Private method (uses closure to access internal elements)\n_validateAndUpdate(state, indicator) {\n// Simple validation logic\nstate.isValid = state.value.length >= 8;\n// Update UI to reflect state\nindicator.className = state.isValid\n? 'status-indicator valid'\n: 'status-indicator invalid';\n}\n// Lifecycle methods still work normally\nconnectedCallback() {\nconsole.log('Secure component connected');\n}\ndisconnectedCallback() {\nconsole.log('Secure component disconnected');\n}\n}\ncustomElements.define('secure-component', SecureComponent);\nThis secure component demonstrates key principles of closed shadow DOM usage:\nPrivate State Management: State is maintained in closure, inaccessible to external scripts\nExplicit API Design: Functionality is exposed through carefully designed methods on the element\nEvent-Based Communication: Changes are announced through custom events (the component calls this.dispatchEvent(new CustomEvent('name', { detail })) and the page listens with addEventListener)\nStyle Encapsulation: Styles are fully contained and inaccessible externally\nTechnical Considerations and Tradeoffs\nWhile mode: 'closed' offers stronger encapsulation, it comes with significant tradeoffs:\nDebugging Challenges: Inspecting and debugging closed shadow DOMs is more difficult\nTesting Complexity: Unit tests can't easily access internal elements\nExternal Styling Limitations: Makes providing styling hooks slightly more complex\nThird-Party Extensions: Prevents legitimate extensions or enhancements\nDeveloper Experience: Can create friction for legitimate component users\nMost component libraries choose mode: 'open' for these reasons, as it provides sufficient encapsulation while maintaining better developer experience. Reserve mode: 'closed' for cases where the enhanced security and encapsulation are explicitly required by the component's purpose.\nNote on Lifecycle Implications: While shadow DOM mode doesn't directly affect component lifecycle methods like connectedCallback or disconnectedCallback, it does influence how those methods interact with the component's internal DOM. With closed mode, lifecycle methods need to rely on closure variables or instance properties to manipulate the shadow DOM, as the lifecycle post (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/) explains.\nAdvanced Styling Architectures\n1. Dynamic Style Injection with Performance Optimization (advanced, optional)\nFor components requiring dynamic styling based on state or props, a more sophisticated approach involves programmatically generating and injecting styles with performance considerations:\nclass DynamicStyledComponent extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['variant', 'size', 'disabled'];\n}\nconstructor() {\nsuper();\nthis.shadow = this.attachShadow({ mode: 'open' });\nthis._styleElement = document.createElement('style');\nthis.shadow.appendChild(this._styleElement);\n// Create the component structure\nconst container = document.createElement('div');\ncontainer.className = 'container';\ncontainer.innerHTML = '<slot></slot>';\nthis.shadow.appendChild(container);\n// Initial render\nthis._updateStyles();\n}\nattributeChangedCallback() {\n// Debounced style updates for performance\nif (this._styleUpdateScheduled) return;\nthis._styleUpdateScheduled = true;\nrequestAnimationFrame(() => {\nthis._updateStyles();\nthis._styleUpdateScheduled = false;\n});\n}\n_updateStyles() {\nconst variant = this.getAttribute('variant') || 'default';\nconst size = this.getAttribute('size') || 'medium';\nconst disabled = this.hasAttribute('disabled');\nconst variantStyles = {\ndefault: {\nbackgroundColor: 'var(--color-surface, white)',\ncolor: 'var(--color-text, #333)'\n},\nprimary: {\nbackgroundColor: 'var(--color-primary, #0070f3)',\ncolor: 'var(--color-primary-contrast, white)'\n},\n// Additional variants\n};\nconst sizeStyles = {\nsmall: { padding: '8px 12px', fontSize: '14px' },\nmedium: { padding: '12px 16px', fontSize: '16px' },\nlarge: { padding: '16px 24px', fontSize: '18px' }\n};\nconst selectedVariant = variantStyles[variant] || variantStyles.default;\nconst selectedSize = sizeStyles[size] || sizeStyles.medium;\n// Generate optimized CSS\nthis._styleElement.textContent = `\n.container {\n${Object.entries(selectedVariant).map(([prop, value]) => `${prop}: ${value};`).join('\\n        ')}\n${Object.entries(selectedSize).map(([prop, value]) => `${prop}: ${prop}: ${value};`).join('\\n        ')}\n${disabled ? 'opacity: 0.6; pointer-events: none;' : ''}\nborder-radius: var(--border-radius, 4px);\ntransition: box-shadow 150ms ease-in-out;\n}\n.container:hover {\n${disabled ? '' : 'box-shadow: var(--shadow-hover, 0 4px 8px rgba(0,0,0,0.1));'}\n}\n`;\n}\n}\ncustomElements.define('dynamic-styled-component', DynamicStyledComponent);\nThis implementation demonstrates several advanced techniques:\nAttribute-driven styling with observed attributes\nPerformance optimization via RAF debouncing\nDynamic style generation based on component state\nConditional style application\nThis demonstration brings to life the dynamic styling patterns we've just explored. Experiment with different variants, sizes, and states to see how attribute-driven styling transforms the component in real-time. The implementation uses requestAnimationFrame for performance optimization and demonstrates how CSS custom properties create a flexible styling API while maintaining visual consistency. Try toggling between light and dark modes to see how the component adapts to different color schemes.\nHTML\n<h1>Dynamic Web Component Styling</h1>\n<p>This demonstration shows a dynamic web component whose appearance updates in real-time based on attributes.</p>\n<div class=\"configuration-panel\">\n<h3>Component Configuration</h3>\n<div class=\"control-row\">\n<label>\nVariant:\n<select id=\"variant-select\">\n<option value=\"primary\">Primary</option>\n<option value=\"success\">Success</option>\n<option value=\"warning\">Warning</option>\n<option value=\"danger\">Danger</option>\n<option value=\"info\">Info</option>\n</select>\n</label>\n<label>\nSize:\n<select id=\"size-select\">\n<option value=\"small\">Small</option>\n<option value=\"medium\" selected>Medium</option>\n<option value=\"large\">Large</option>\n</select>\n</label>\n<label>\n<input type=\"checkbox\" id=\"disabled-checkbox\">\nDisabled\n</label>\n<label>\n<input type=\"checkbox\" id=\"rounded-checkbox\" checked>\nRounded\n</label>\n<label>\n<input type=\"checkbox\" id=\"outlined-checkbox\">\nOutlined\n</label>\n</div>\n<div class=\"control-row\">\n<label>\nContent:\n<input type=\"text\" id=\"content-input\" value=\"Dynamic Component\" style=\"width: 200px;\">\n</label>\n<button id=\"update-content\">Update Content</button>\n</div>\n</div>\n<div class=\"demo-container\">\n<h3>Demo</h3>\n<dynamic-card variant=\"primary\" size=\"medium\" rounded>\nDynamic Component\n</dynamic-card>\n</div>\n<div class=\"toggle-wrapper\">\n<button id=\"toggle-theme\">Toggle Light/Dark Mode</button>\n</div>\nCSS\n:root {\n/* Light theme variables */\n--bg-color: #f8f9fa;\n--text-color: #212529;\n--border-color: #dee2e6;\n--card-bg: #fff;\n--control-bg: #e9ecef;\n--control-border: #ced4da;\n--primary-color: #0d6efd;\n--success-color: #198754;\n--warning-color: #ffc107;\n--danger-color: #dc3545;\n--info-color: #0dcaf0;\ncolor-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n:root {\n--bg-color: #212529;\n--text-color: #f8f9fa;\n--border-color: #495057;\n--card-bg: #343a40;\n--control-bg: #495057;\n--control-border: #6c757d;\n--primary-color: #0d6efd;\n--success-color: #198754;\n--warning-color: #ffc107;\n--danger-color: #dc3545;\n--info-color: #0dcaf0;\n}\n}\nbody {\nfont-family: system-ui, -apple-system, sans-serif;\nbackground-color: var(--bg-color);\ncolor: var(--text-color);\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\ntransition: background-color 0.3s ease;\nline-height: 1.5;\n}\n.configuration-panel {\nmargin-bottom: 20px;\npadding: 20px;\nborder-radius: 8px;\nbackground-color: var(--card-bg);\nborder: 1px solid var(--border-color);\n}\n.control-row {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\nmargin-bottom: 10px;\n}\nbutton {\npadding: 8px 16px;\nbackground-color: var(--primary-color);\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\ntransition: opacity 0.2s;\n}\nbutton:hover {\nopacity: 0.9;\n}\nselect,\ninput {\npadding: 8px;\nborder: 1px solid var(--control-border);\nborder-radius: 4px;\nbackground-color: var(--control-bg);\ncolor: var(--text-color);\n}\nlabel {\nmargin-right: 8px;\ndisplay: flex;\nalign-items: center;\n}\nh1,\nh2,\nh3 {\ncolor: var(--text-color);\n}\n.demo-container {\npadding: 20px;\nborder-radius: 8px;\nbackground-color: var(--card-bg);\nborder: 1px solid var(--border-color);\n}\n.toggle-wrapper {\nmargin-top: 20px;\ndisplay: flex;\njustify-content: flex-end;\n}\nJavaScript\nclass DynamicCard extends HTMLElement {\nstatic get observedAttributes() {\nreturn [\"variant\", \"size\", \"disabled\", \"rounded\", \"outlined\"];\n}\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis._styleElement = document.createElement(\"style\");\nthis.shadowRoot.appendChild(this._styleElement);\nthis._container = document.createElement(\"div\");\nthis._container.className = \"dynamic-card\";\nthis._container.innerHTML = \"<slot></slot>\";\nthis.shadowRoot.appendChild(this._container);\nthis._updateStyles();\n}\nattributeChangedCallback() {\n// Debounced style updates for performance\nif (this._styleUpdateScheduled) return;\nthis._styleUpdateScheduled = true;\nrequestAnimationFrame(() => {\nthis._updateStyles();\nthis._styleUpdateScheduled = false;\n});\n}\n_updateStyles() {\nconst variant = this.getAttribute(\"variant\") || \"primary\";\nconst size = this.getAttribute(\"size\") || \"medium\";\nconst disabled = this.hasAttribute(\"disabled\");\nconst rounded = this.hasAttribute(\"rounded\");\nconst outlined = this.hasAttribute(\"outlined\");\nconst variantStyles = {\nprimary: {\nbackgroundColor: outlined ? \"transparent\" : \"var(--primary-color, #0d6efd)\",\ncolor: outlined ? \"var(--primary-color, #0d6efd)\" : \"white\",\nborderColor: \"var(--primary-color, #0d6efd)\" },\nsuccess: {\nbackgroundColor: outlined ? \"transparent\" : \"var(--success-color, #198754)\",\ncolor: outlined ? \"var(--success-color, #198754)\" : \"white\",\nborderColor: \"var(--success-color, #198754)\" },\nwarning: {\nbackgroundColor: outlined ? \"transparent\" : \"var(--warning-color, #ffc107)\",\ncolor: outlined ? \"var(--warning-color, #ffc107)\" : \"black\",\nborderColor: \"var(--warning-color, #ffc107)\" },\ndanger: {\nbackgroundColor: outlined ? \"transparent\" : \"var(--danger-color, #dc3545)\",\ncolor: outlined ? \"var(--danger-color, #dc3545)\" : \"white\",\nborderColor: \"var(--danger-color, #dc3545)\" },\ninfo: {\nbackgroundColor: outlined ? \"transparent\" : \"var(--info-color, #0dcaf0)\",\ncolor: outlined ? \"var(--info-color, #0dcaf0)\" : \"black\",\nborderColor: \"var(--info-color, #0dcaf0)\" } };\nconst sizeStyles = {\nsmall: {\npadding: \"8px 12px\",\nfontSize: \"14px\" },\nmedium: {\npadding: \"12px 20px\",\nfontSize: \"16px\" },\nlarge: {\npadding: \"16px 28px\",\nfontSize: \"18px\" } };\nconst selectedVariant = variantStyles[variant] || variantStyles.primary;\nconst selectedSize = sizeStyles[size] || sizeStyles.medium;\n// Generate optimized CSS\nthis._styleElement.textContent = `\n.dynamic-card {\ndisplay: inline-block;\nbackground-color: ${selectedVariant.backgroundColor};\ncolor: ${selectedVariant.color};\nborder: 2px solid ${selectedVariant.borderColor};\n${Object.entries(selectedSize).\nmap(([prop, value]) => `${prop}: ${value};`).\njoin(\"\\n            \")}\nborder-radius: ${rounded ? \"50px\" : \"4px\"};\ntransition: all 0.2s ease-in-out;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n${\ndisabled ?\n\"opacity: 0.6; cursor: not-allowed; pointer-events: none;\" :\n\"cursor: pointer;\"\n}\n}\n.dynamic-card:hover {\n${\ndisabled ?\n\"\" :\n\"transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15);\"\n}\n}\n`;\n}}\ncustomElements.define(\"dynamic-card\", DynamicCard);\n// UI control functionality\nconst dynamicCard = document.querySelector(\"dynamic-card\");\ndocument.getElementById(\"variant-select\").addEventListener(\"change\", e => {\ndynamicCard.setAttribute(\"variant\", e.target.value);\n});\ndocument.getElementById(\"size-select\").addEventListener(\"change\", e => {\ndynamicCard.setAttribute(\"size\", e.target.value);\n});\ndocument.getElementById(\"disabled-checkbox\").addEventListener(\"change\", e => {\nif (e.target.checked) {\ndynamicCard.setAttribute(\"disabled\", \"\");\n} else {\ndynamicCard.removeAttribute(\"disabled\");\n}\n});\ndocument.getElementById(\"rounded-checkbox\").addEventListener(\"change\", e => {\nif (e.target.checked) {\ndynamicCard.setAttribute(\"rounded\", \"\");\n} else {\ndynamicCard.removeAttribute(\"rounded\");\n}\n});\ndocument.getElementById(\"outlined-checkbox\").addEventListener(\"change\", e => {\nif (e.target.checked) {\ndynamicCard.setAttribute(\"outlined\", \"\");\n} else {\ndynamicCard.removeAttribute(\"outlined\");\n}\n});\ndocument.getElementById(\"update-content\").addEventListener(\"click\", () => {\nconst content = document.getElementById(\"content-input\").value;\ndynamicCard.textContent = content;\n});\ndocument.getElementById(\"toggle-theme\").addEventListener(\"click\", () => {\n// Check current background color to determine current theme\nconst isDark = getComputedStyle(document.body).backgroundColor.match(\n/rgb\\(33,\\s*37,\\s*41\\)/);\nif (isDark) {\ndocument.documentElement.style.setProperty(\"--bg-color\", \"#f8f9fa\");\ndocument.documentElement.style.setProperty(\"--text-color\", \"#212529\");\ndocument.documentElement.style.setProperty(\"--border-color\", \"#dee2e6\");\ndocument.documentElement.style.setProperty(\"--card-bg\", \"#fff\");\ndocument.documentElement.style.setProperty(\"--control-bg\", \"#e9ecef\");\ndocument.documentElement.style.setProperty(\"--control-border\", \"#ced4da\");\n} else {\ndocument.documentElement.style.setProperty(\"--bg-color\", \"#212529\");\ndocument.documentElement.style.setProperty(\"--text-color\", \"#f8f9fa\");\ndocument.documentElement.style.setProperty(\"--border-color\", \"#495057\");\ndocument.documentElement.style.setProperty(\"--card-bg\", \"#343a40\");\ndocument.documentElement.style.setProperty(\"--control-bg\", \"#495057\");\ndocument.documentElement.style.setProperty(\"--control-border\", \"#6c757d\");\n}\n});\nDynamic Styling Component on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\n2. Constructable Stylesheets for Runtime Optimization (advanced, optional)\nSo far each component has carried its own <style> tag, so a hundred cards means a hundred copies of the same CSS. A constructable stylesheet is a stylesheet you create in JavaScript once and share. new CSSStyleSheet() makes an empty one, replaceSync(cssText) fills it with CSS immediately (no network, no waiting), and shadow.adoptedStyleSheets = [sheet] lets a shadow root use it. Many components can adopt the same sheet object. This is an optimization, not something you need on day one:\n// Create module-scoped stylesheets\nconst baseStyles = new CSSStyleSheet();\nbaseStyles.replaceSync(`\n:host {\ndisplay: block;\nfont-family: var(--font-family, system-ui);\n}\n.base {\nbox-sizing: border-box;\nwidth: 100%;\n}\n`);\nconst themeStyles = new CSSStyleSheet();\nthemeStyles.replaceSync(`\n.themed {\ncolor: var(--text-color, #212121);\nbackground: var(--background-color, #ffffff);\n}\n`);\n// Component implementation with shared stylesheets\nclass OptimizedComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst shadow = this.attachShadow({ mode: 'open' });\n// Adopt shared stylesheets (no duplication in memory)\nshadow.adoptedStyleSheets = [baseStyles, themeStyles];\n// Create component structure\nshadow.innerHTML = `\n<div class=\"base themed\">\n<slot></slot>\n</div>\n`;\n}\n}\ncustomElements.define('optimized-component', OptimizedComponent);\nThe technical advantages of this approach include:\nSheet objects are parsed once but used by multiple component instances\nMemory usage is significantly reduced compared to duplicated inline styles\nBrowser rendering optimizations can be applied to shared stylesheets\nUpdates to shared stylesheets automatically propagate to all components\nCreating a Component Design System with CSS Custom Properties\nTo implement a cohesive design system through Web Components, a systematic approach to CSS custom properties creates a powerful styling API:\n// design-system.js\nconst designSystem = {\n// Define all design tokens at the :root level\ninstallGlobalTokens() {\nconst tokens = document.createElement('style');\ntokens.textContent = `\n:root {\n/* Color system */\n--color-primary-50: #e3f2fd;\n--color-primary-100: #bbdefb;\n--color-primary-500: #2196f3;\n--color-primary-900: #0d47a1;\n/* Typography */\n--font-size-xs: 12px;\n--font-size-sm: 14px;\n--font-size-md: 16px;\n--font-size-lg: 18px;\n--font-size-xl: 20px;\n/* Spacing system */\n--spacing-unit: 4px;\n--spacing-xs: calc(var(--spacing-unit) * 1);\n--spacing-sm: calc(var(--spacing-unit) * 2);\n--spacing-md: calc(var(--spacing-unit) * 4);\n--spacing-lg: calc(var(--spacing-unit) * 6);\n--spacing-xl: calc(var(--spacing-unit) * 10);\n/* Elevation */\n--shadow-1: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);\n--shadow-2: 0 3px 6px rgba(0,0,0,0.15), 0 2px 4px rgba(0,0,0,0.12);\n--shadow-3: 0 10px 20px rgba(0,0,0,0.15), 0 3px 6px rgba(0,0,0,0.10);\n/* Border radius */\n--radius-sm: 2px;\n--radius-md: 4px;\n--radius-lg: 8px;\n--radius-full: 999px;\n/* Animation */\n--duration-fast: 150ms;\n--duration-normal: 300ms;\n--duration-slow: 500ms;\n--easing-standard: cubic-bezier(0.4, 0.0, 0.2, 1);\n}\n`;\ndocument.head.appendChild(tokens);\n},\n// Component tokens derived from global tokens\ngetComponentTokens(name) {\nconst style = document.createElement('style');\n// Component-specific custom properties derived from global tokens\nconst componentTokens = {\n'ds-button': `\n--button-bg: var(--color-primary-500);\n--button-color: white;\n--button-padding: var(--spacing-sm) var(--spacing-md);\n--button-radius: var(--radius-md);\n--button-shadow: var(--shadow-1);\n`,\n'ds-card': `\n--card-bg: white;\n--card-border: 1px solid rgba(0,0,0,0.1);\n--card-padding: var(--spacing-md);\n--card-radius: var(--radius-md);\n--card-shadow: var(--shadow-1);\n`\n};\nstyle.textContent = `\n${name} {\n${componentTokens[name] || ''}\n}\n`;\nreturn style;\n}\n};\n// Install design system tokens\ndocument.addEventListener('DOMContentLoaded', () => {\ndesignSystem.installGlobalTokens();\n});\nThis approach enables several advanced styling capabilities:\nCentralized design token management\nHierarchical theming with cascading defaults\nComponent-specific styling APIs that respect the global design system\nRuntime theming capabilities without component modification\nTry a design system built with CSS custom properties. This interactive example showcases how design tokens cascade through your component hierarchy, creating a consistent visual language. The design system automatically adapts between light and dark modes while maintaining component-specific styling APIs. Explore the color tokens, typography scale, and spacing system that form the foundation of the design system, then see how they're applied to create cohesive components that respond to their environment.\nHTML\n<div class=\"container\">\n<div class=\"toolbar\">\n<h1>Design System with CSS Custom Properties</h1>\n<button id=\"theme-toggle\">\n<span id=\"theme-icon\">🌓</span>\n<span id=\"theme-text\">Toggle Theme</span>\n</button>\n</div>\n<p>\nThis demo shows how Web Components can use CSS Custom Properties to create a consistent\ndesign system that responds to light/dark mode preferences and supports theming.\n</p>\n<div class=\"demo-section\">\n<h2>Color Tokens</h2>\n<p>The foundation of our design system starts with color tokens:</p>\n<div class=\"token-grid\">\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--theme-primary);\"></div>\n<div class=\"token-name\">--theme-primary</div>\n<div class=\"token-value\">#4361ee</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--theme-secondary);\"></div>\n<div class=\"token-name\">--theme-secondary</div>\n<div class=\"token-value\">#3f37c9</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--theme-accent);\"></div>\n<div class=\"token-name\">--theme-accent</div>\n<div class=\"token-value\">#4cc9f0</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--theme-success);\"></div>\n<div class=\"token-name\">--theme-success</div>\n<div class=\"token-value\">#4caf50</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--theme-warning);\"></div>\n<div class=\"token-name\">--theme-warning</div>\n<div class=\"token-value\">#ff9800</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--theme-danger);\"></div>\n<div class=\"token-name\">--theme-danger</div>\n<div class=\"token-value\">#f72585</div>\n</div>\n</div>\n<h3>Semantic Colors</h3>\n<p>These adapt automatically between light and dark mode:</p>\n<div class=\"token-grid\">\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--surface-bg);\"></div>\n<div class=\"token-name\">--surface-bg</div>\n<div class=\"token-value\">(dynamic)</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--surface-card);\"></div>\n<div class=\"token-name\">--surface-card</div>\n<div class=\"token-value\">(dynamic)</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--text-primary);\"></div>\n<div class=\"token-name\">--text-primary</div>\n<div class=\"token-value\">(dynamic)</div>\n</div>\n<div class=\"token-item\">\n<div class=\"color-preview\" style=\"background-color: var(--border-color);\"></div>\n<div class=\"token-name\">--border-color</div>\n<div class=\"token-value\">(dynamic)</div>\n</div>\n</div>\n</div>\n<div class=\"demo-section\">\n<h2>Component Examples</h2>\n<p>These web components use our design system tokens to maintain consistency:</p>\n<div class=\"component-grid\">\n<ds-card>\n<h3 slot=\"title\">Design System Card</h3>\n<p slot=\"content\">This card component automatically adapts to light/dark mode and uses our spacing, typography, and color tokens.</p>\n<div slot=\"footer\">\n<ds-button variant=\"primary\">Primary Action</ds-button>\n<ds-button variant=\"secondary\">Secondary</ds-button>\n</div>\n</ds-card>\n<ds-card>\n<h3 slot=\"title\">Interactive Controls</h3>\n<div slot=\"content\">\n<p>Try these buttons that follow our design system:</p>\n<div style=\"display: flex; gap: var(--spacing-sm); flex-wrap: wrap; margin-bottom: var(--spacing-md);\">\n<ds-button variant=\"primary\">Primary</ds-button>\n<ds-button variant=\"secondary\">Secondary</ds-button>\n<ds-button variant=\"accent\">Accent</ds-button>\n<ds-button variant=\"success\">Success</ds-button>\n<ds-button variant=\"warning\">Warning</ds-button>\n<ds-button variant=\"danger\">Danger</ds-button>\n</div>\n<p>Size variants:</p>\n<div style=\"display: flex; gap: var(--spacing-sm); align-items: center;\">\n<ds-button variant=\"primary\" size=\"small\">Small</ds-button>\n<ds-button variant=\"primary\" size=\"medium\">Medium</ds-button>\n<ds-button variant=\"primary\" size=\"large\">Large</ds-button>\n</div>\n</div>\n</ds-card>\n</div>\n</div>\n</div>\nCSS\n/* Design System Tokens */\n:root {\n/* Theme Colors */\n--theme-primary: #4361ee;\n--theme-secondary: #3f37c9;\n--theme-accent: #4cc9f0;\n--theme-success: #4caf50;\n--theme-warning: #ff9800;\n--theme-danger: #f72585;\n/* Neutral Colors - Light Mode Default */\n--neutral-50: #f8f9fa;\n--neutral-100: #f0f0f0;\n--neutral-200: #e9ecef;\n--neutral-300: #dee2e6;\n--neutral-400: #ced4da;\n--neutral-500: #adb5bd;\n--neutral-600: #6c757d;\n--neutral-700: #495057;\n--neutral-800: #343a40;\n--neutral-900: #212529;\n/* Semantic Mappings - Light Mode */\n--surface-bg: var(--neutral-50);\n--surface-card: white;\n--text-primary: var(--neutral-900);\n--text-secondary: var(--neutral-700);\n--text-tertiary: var(--neutral-600);\n--border-color: var(--neutral-300);\n/* Spacing System */\n--spacing-unit: 4px;\n--spacing-xs: calc(var(--spacing-unit) * 1); /* 4px */\n--spacing-sm: calc(var(--spacing-unit) * 2); /* 8px */\n--spacing-md: calc(var(--spacing-unit) * 4); /* 16px */\n--spacing-lg: calc(var(--spacing-unit) * 6); /* 24px */\n--spacing-xl: calc(var(--spacing-unit) * 10); /* 40px */\n/* Typography */\n--font-family: system-ui, -apple-system, sans-serif;\n--font-size-xs: 12px;\n--font-size-sm: 14px;\n--font-size-md: 16px;\n--font-size-lg: 20px;\n--font-size-xl: 24px;\n--font-size-2xl: 32px;\n/* Border Radius */\n--radius-sm: 4px;\n--radius-md: 8px;\n--radius-lg: 16px;\n--radius-xl: 24px;\n--radius-full: 9999px;\n/* Shadows */\n--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);\n--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);\n--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);\n/* Animation */\n--duration-fast: 150ms;\n--duration-normal: 300ms;\n--duration-slow: 500ms;\n--easing-standard: cubic-bezier(0.4, 0, 0.2, 1);\n/* Component Specific - Button */\n--button-padding-x: var(--spacing-md);\n--button-padding-y: var(--spacing-sm);\n--button-radius: var(--radius-md);\n--button-font-size: var(--font-size-md);\n/* Component Specific - Card */\n--card-padding: var(--spacing-md);\n--card-radius: var(--radius-md);\n--card-shadow: var(--shadow-md);\n--card-border: 1px solid var(--border-color);\ncolor-scheme: light dark;\n}\n/* Dark Mode Overrides */\n@media (prefers-color-scheme: dark) {\n:root {\n/* Semantic Mappings - Dark Mode */\n--surface-bg: var(--neutral-900);\n--surface-card: var(--neutral-800);\n--text-primary: var(--neutral-100);\n--text-secondary: var(--neutral-300);\n--text-tertiary: var(--neutral-400);\n--border-color: var(--neutral-700);\n}\n}\n/* Base Styles */\nbody {\nfont-family: var(--font-family);\nbackground-color: var(--surface-bg);\ncolor: var(--text-primary);\nline-height: 1.5;\nmargin: 0;\npadding: var(--spacing-md);\ntransition: background-color var(--duration-normal) var(--easing-standard);\n}\n.container {\nmax-width: 1200px;\nmargin: 0 auto;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\ncolor: var(--text-primary);\nmargin-top: 0;\n}\nh1 {\nfont-size: var(--font-size-2xl);\nmargin-bottom: var(--spacing-lg);\n}\nh2 {\nfont-size: var(--font-size-xl);\nmargin-bottom: var(--spacing-md);\nmargin-top: var(--spacing-xl);\n}\nh3 {\nfont-size: var(--font-size-lg);\nmargin-bottom: var(--spacing-sm);\n}\np {\nmargin-bottom: var(--spacing-md);\ncolor: var(--text-secondary);\n}\nbutton {\ncursor: pointer;\n}\n.demo-section {\nbackground-color: var(--surface-card);\nborder-radius: var(--radius-md);\npadding: var(--spacing-lg);\nmargin-bottom: var(--spacing-xl);\nborder: 1px solid var(--border-color);\n}\n.token-grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fill, minmax(250px, 1fr));\ngap: var(--spacing-md);\nmargin: var(--spacing-lg) 0;\n}\n.token-item {\npadding: var(--spacing-md);\nborder-radius: var(--radius-sm);\nborder: 1px solid var(--border-color);\nbackground-color: var(--surface-card);\n}\n.token-name {\nfont-family: monospace;\nfont-size: var(--font-size-sm);\ncolor: var(--theme-primary);\nmargin-bottom: var(--spacing-xs);\n}\n.token-value {\nfont-family: monospace;\nfont-size: var(--font-size-sm);\ncolor: var(--text-tertiary);\n}\n.color-preview {\nheight: 30px;\nborder-radius: var(--radius-sm);\nmargin-bottom: var(--spacing-xs);\n}\n.component-grid {\ndisplay: grid;\ngrid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\ngap: var(--spacing-xl);\nmargin: var(--spacing-lg) 0;\n}\n.toolbar {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nmargin-bottom: var(--spacing-lg);\n}\n#theme-toggle {\nbackground-color: var(--surface-card);\ncolor: var(--text-primary);\nborder: 1px solid var(--border-color);\npadding: var(--spacing-sm) var(--spacing-md);\nborder-radius: var(--radius-full);\nfont-size: var(--font-size-sm);\ndisplay: flex;\nalign-items: center;\ngap: var(--spacing-xs);\ntransition: all var(--duration-fast) var(--easing-standard);\n}\n#theme-toggle:hover {\nbackground-color: var(--neutral-200);\n}\n@media (prefers-color-scheme: dark) {\n#theme-toggle:hover {\nbackground-color: var(--neutral-700);\n}\n}\nJavaScript\n// Design System Button Component\nclass DesignSystemButton extends HTMLElement {\nstatic get observedAttributes() {\nreturn [\"variant\", \"size\", \"disabled\"];\n}\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.render();\n}\nconnectedCallback() {\nthis.shadowRoot.querySelector(\"button\").addEventListener(\"click\", e => {\nif (!this.hasAttribute(\"disabled\")) {\nthis.dispatchEvent(\nnew CustomEvent(\"click\", {\nbubbles: true,\ncomposed: true,\ndetail: { originalEvent: e } }));\n}\n});\n}\nattributeChangedCallback() {\nthis.render();\n}\ndisconnectedCallback() {\nthis.shadowRoot.\nquerySelector(\"button\").\nremoveEventListener(\"click\", this.handleClick);\n}\nrender() {\nconst variant = this.getAttribute(\"variant\") || \"primary\";\nconst size = this.getAttribute(\"size\") || \"medium\";\nconst disabled = this.hasAttribute(\"disabled\");\nconst buttonSizes = {\nsmall: {\n\"--button-font-size\": \"var(--font-size-sm)\",\n\"--button-padding\": \"var(--spacing-xs) var(--spacing-sm)\" },\nmedium: {\n\"--button-font-size\": \"var(--font-size-md)\",\n\"--button-padding\": \"var(--spacing-sm) var(--spacing-md)\" },\nlarge: {\n\"--button-font-size\": \"var(--font-size-lg)\",\n\"--button-padding\": \"var(--spacing-md) var(--spacing-lg)\" } };\nconst sizeStyle = buttonSizes[size] || buttonSizes.medium;\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: inline-block;\n}\nbutton {\nfont-family: var(--font-family);\nfont-size: var(--button-font-size);\npadding: var(--button-padding);\nborder-radius: var(--button-radius, var(--radius-md));\nborder: none;\ncursor: ${disabled ? \"not-allowed\" : \"pointer\"};\nopacity: ${disabled ? \"0.6\" : \"1\"};\ntransition: all var(--duration-fast) var(--easing-standard);\ncolor: white;\nbackground-color: var(--theme-${variant}, var(--theme-primary));\n}\nbutton.secondary {\nbackground-color: transparent;\ncolor: var(--theme-${variant}, var(--theme-primary));\nborder: 1px solid var(--theme-${variant}, var(--theme-primary));\n}\nbutton:hover:not(:disabled) {\nfilter: brightness(1.1);\ntransform: translateY(-1px);\n}\nbutton:active:not(:disabled) {\nfilter: brightness(0.9);\ntransform: translateY(0);\n}\nbutton:focus-visible {\noutline: 2px solid var(--theme-${variant}, var(--theme-primary));\noutline-offset: 2px;\n}\n</style>\n<button\nclass=\"${variant === \"secondary\" ? \"secondary\" : \"\"}\"\n?disabled=\"${disabled}\"\nstyle=\"${Object.entries(sizeStyle).\nmap(([prop, val]) => `${prop}: ${val}`).\njoin(\";\")}\">\n<slot></slot>\n</button>\n`;\n}}\n// Design System Card Component\nclass DesignSystemCard extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\nthis.render();\n}\nrender() {\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\n}\n.card {\nbackground-color: var(--surface-card);\nborder-radius: var(--card-radius);\nbox-shadow: var(--card-shadow);\nborder: var(--card-border);\noverflow: hidden;\ntransition: all var(--duration-normal) var(--easing-standard);\n}\n.card:hover {\nbox-shadow: var(--shadow-lg);\ntransform: translateY(-2px);\n}\n.card-header {\npadding: var(--card-padding);\nborder-bottom: 1px solid var(--border-color);\n}\n.card-body {\npadding: var(--card-padding);\n}\n.card-footer {\npadding: var(--card-padding);\nborder-top: 1px solid var(--border-color);\ndisplay: flex;\ngap: var(--spacing-sm);\nflex-wrap: wrap;\n}\n::slotted(h3) {\nmargin-top: 0;\nmargin-bottom: 0;\n}\n</style>\n<div class=\"card\">\n<div class=\"card-header\">\n<slot name=\"title\"></slot>\n</div>\n<div class=\"card-body\">\n<slot name=\"content\"></slot>\n</div>\n<div class=\"card-footer\">\n<slot name=\"footer\"></slot>\n</div>\n</div>\n`;\n}}\n// Register custom elements\ncustomElements.define(\"ds-button\", DesignSystemButton);\ncustomElements.define(\"ds-card\", DesignSystemCard);\n// Theme toggle functionality\ndocument.getElementById(\"theme-toggle\").addEventListener(\"click\", () => {\nconst prefersDark = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\nconst rootStyle = document.documentElement.style;\n// If system prefers dark and we're in dark mode OR system prefers light and we're in light mode\nconst isUsingSystemTheme = !rootStyle.getPropertyValue(\"--surface-bg\");\nif (isUsingSystemTheme) {\nif (prefersDark) {\n// System prefers dark, switch to light\nrootStyle.setProperty(\"--surface-bg\", \"var(--neutral-50)\");\nrootStyle.setProperty(\"--surface-card\", \"white\");\nrootStyle.setProperty(\"--text-primary\", \"var(--neutral-900)\");\nrootStyle.setProperty(\"--text-secondary\", \"var(--neutral-700)\");\nrootStyle.setProperty(\"--text-tertiary\", \"var(--neutral-600)\");\nrootStyle.setProperty(\"--border-color\", \"var(--neutral-300)\");\ndocument.getElementById(\"theme-text\").textContent = \"Light Mode\";\ndocument.getElementById(\"theme-icon\").textContent = \"☀️\";\n} else {\n// System prefers light, switch to dark\nrootStyle.setProperty(\"--surface-bg\", \"var(--neutral-900)\");\nrootStyle.setProperty(\"--surface-card\", \"var(--neutral-800)\");\nrootStyle.setProperty(\"--text-primary\", \"var(--neutral-100)\");\nrootStyle.setProperty(\"--text-secondary\", \"var(--neutral-300)\");\nrootStyle.setProperty(\"--text-tertiary\", \"var(--neutral-400)\");\nrootStyle.setProperty(\"--border-color\", \"var(--neutral-700)\");\ndocument.getElementById(\"theme-text\").textContent = \"Dark Mode\";\ndocument.getElementById(\"theme-icon\").textContent = \"🌙\";\n}\n} else {\n// We're using a manual theme, reset to system preference\nrootStyle.removeProperty(\"--surface-bg\");\nrootStyle.removeProperty(\"--surface-card\");\nrootStyle.removeProperty(\"--text-primary\");\nrootStyle.removeProperty(\"--text-secondary\");\nrootStyle.removeProperty(\"--text-tertiary\");\nrootStyle.removeProperty(\"--border-color\");\ndocument.getElementById(\"theme-text\").textContent = \"System Theme\";\ndocument.getElementById(\"theme-icon\").textContent = \"🌓\";\n}\n});\nCSS Custom Properties Design System on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\nChallenge: override a token from the page. The card below reads its padding from --spacing-md, falling back to 16px. Run it, then add one rule to the page CSS (the TODO) that sets --spacing-md on my-card. Expected result: the card's padding grows to 40px and the whole card gets visibly roomier. Custom properties are inherited, so they travel from the page into the shadow tree even though ordinary selectors cannot.\nHTML\n<my-card><p>Hello from the light DOM.</p></my-card>\nCSS\nbody { font-family: sans-serif; }\n/* TODO: add  my-card { --spacing-md: 40px; }  below this line */\nJavaScript\nclass MyCard extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' }).innerHTML = `\n<style>\n.box {\npadding: var(--spacing-md, 16px);\nborder: 2px solid steelblue;\n}\n</style>\n<div class=\"box\"><slot></slot></div>`;\n}\n}\ncustomElements.define('my-card', MyCard);\nChallenge: override --spacing-md on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\nTip:\nThe rule goes in the CSS tab, not inside the component. Try --spacing-md: 4px too and watch the card shrink.\nQuestion: A page has the rules my-card { … }, my-card p { … } and my-card .box { … }. The <p> is slotted light DOM, and .box is a <div> inside the component's shadow tree. Which elements can these page rules style?\nOnly the my-card host\nThe my-card host and the slotted <p>, but not the inner .box\nAll three, because the page's CSS wins over the component's\nAnswer: 2. The my-card host and the slotted <p>, but not the inner .box\nExplanation: The host and its light DOM children belong to the page's own tree. The inner .box lives in the shadow tree, so page selectors never match it.\nThe Fourth Door: ::part\nCustom properties work only when the component chose to read a variable. ::part is the other direction: the component names an inner element with a part attribute, and the page can then style that element directly.\nHTML\n<my-card><p>Slotted text</p></my-card>\nCSS\nbody { font-family: sans-serif; }\nmy-card::part(action) {\nbackground: tomato;\ncolor: white;\n}\nJavaScript\nclass MyCard extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' }).innerHTML = `\n<slot></slot>\n<button part=\"action\">Save</button>`;\n}\n}\ncustomElements.define('my-card', MyCard);\nStyling a named part from the page on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\nDelete the part=\"action\" attribute and run again: the button goes back to its default look, because the page can only reach what the component exposes. One limit worth knowing: ::part(action) styles that element itself, not its children, so ::part(action) span matches nothing.\nInheritance and Context Adaptation Techniques\nFor components that need to adapt to their environment while maintaining encapsulation, sophisticated context-handling techniques can be employed:\nclass ContextAwareComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst shadow = this.attachShadow({ mode: 'open' });\nshadow.innerHTML = `\n<style>\n/* Base responsive container that adapts to context */\n:host {\ndisplay: block;\n/* Inherit typography properties purposefully */\nfont-family: inherit;\nfont-size: inherit;\nline-height: inherit;\ncolor: inherit;\n/* Default containment for performance */\ncontain: content;\n}\n/* Contextual adaptation */\n:host([data-context=\"card\"]) .content {\npadding: var(--card-inner-spacing, 16px);\n}\n:host([data-context=\"sidebar\"]) .content {\npadding: var(--sidebar-item-spacing, 8px 12px);\n}\n/* Content styles */\n.content {\nbackground-color: var(--component-bg, transparent);\nborder-radius: var(--component-radius, 4px);\ntransition: background-color var(--duration-normal, 300ms) ease;\n}\n/* Advanced slotted content styling */\n::slotted(h1),\n::slotted(h2),\n::slotted(h3) {\nmargin-top: 0;\ncolor: var(--heading-color, currentColor);\n}\n::slotted(p) {\nmargin-bottom: var(--paragraph-spacing, 1em);\n}\n</style>\n<div class=\"content\">\n<slot></slot>\n</div>\n`;\n// Context detection for environment-specific styling\nthis._detectContext();\n// MutationObserver for dynamic context changes\nthis._observer = new MutationObserver(() => this._detectContext());\nthis._observer.observe(this, { attributes: true });\n}\nconnectedCallback() {\n// Re-evaluate context when the component is inserted into a new location\nthis._detectContext();\nthis._startContextObservation();\n}\ndisconnectedCallback() {\nthis._observer.disconnect();\n}\n_startContextObservation() {\n// Walk up the DOM tree to find parent components that might change\nlet parent = this.parentElement;\nwhile (parent) {\nif (parent.tagName?.includes('-')) {\nthis._observer.observe(parent, { attributes: true });\n}\nparent = parent.parentElement;\n}\n}\n_detectContext() {\n// Detect where this component is being used\nconst parentCard = this.closest('card-component');\nconst parentSidebar = this.closest('sidebar-component');\nif (parentCard) {\nthis.setAttribute('data-context', 'card');\n} else if (parentSidebar) {\nthis.setAttribute('data-context', 'sidebar');\n} else {\nthis.removeAttribute('data-context');\n}\n}\n}\ncustomElements.define('context-aware-component', ContextAwareComponent);\nThis implementation demonstrates advanced environment adaptation techniques:\nSelective inheritance of typography properties\nContext detection through DOM traversal\nDynamic adaptation through attribute-based styling\nMutationObserver for responding to contextual changes\nThis demonstration reveals how sophisticated components can intelligently adapt to their surrounding context. The same component automatically adjusts its appearance when placed in different containers (cards vs. sidebars) and responds to theme changes without requiring manual style updates. The implementation uses MutationObserver to detect DOM changes and dynamically updates styling based on the component's environment. Try toggling between container types to see how the component repositions and restyling itself, and switch between light and dark modes to observe theme adaptation in action.\nHTML\n<div class=\"container\">\n<div class=\"title-section\">\n<h1>Context-Aware Web Components</h1>\n<p>Demonstrating how Web Components can adapt to their environment</p>\n</div>\n<div class=\"demo-section\">\n<h2>Component Adaptation to Parent Context</h2>\n<p>The same component adapts its styling based on where it's placed in the UI.</p>\n<div class=\"card-container\">\n<div class=\"card\" id=\"card-context\">\n<div class=\"card-header\">\n<h3>Card Context</h3>\n</div>\n<div class=\"card-body\">\n<p>This panel contains a context-aware component in a card:</p>\n<context-aware-component>\n<h4>Inside a Card</h4>\n<p>This component detects it's inside a card and styles accordingly.</p>\n</context-aware-component>\n</div>\n</div>\n<div class=\"sidebar\" id=\"sidebar-context\">\n<div class=\"sidebar-header\">\n<h3>Sidebar Context</h3>\n</div>\n<div class=\"sidebar-items\">\n<p>The same component in a sidebar:</p>\n<context-aware-component>\n<h4>Inside a Sidebar</h4>\n<p>This component detects it's inside a sidebar and adjusts its appearance.</p>\n</context-aware-component>\n</div>\n</div>\n</div>\n</div>\n<div class=\"demo-section\">\n<h2>Component Adaptation to Theme Changes</h2>\n<p>The component automatically adapts to light/dark mode and react to runtime theme changes.</p>\n<div class=\"card-container\">\n<div class=\"card\">\n<div class=\"card-header\">\n<h3>Default Context</h3>\n</div>\n<div class=\"card-body\">\n<context-aware-component>\n<h4>Default Styling</h4>\n<p>This component is in a standard context and will adapt to light/dark mode.</p>\n</context-aware-component>\n<div class=\"theme-toggle\">\n<button id=\"theme-toggle\">Toggle Light/Dark Mode</button>\n</div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"demo-section\">\n<h2>DOM Hierarchy Observation</h2>\n<p>The component will update its context when parent elements change.</p>\n<div class=\"card-container\">\n<div id=\"dynamic-container\" class=\"card\">\n<div class=\"card-header\">\n<h3>Dynamic Context</h3>\n</div>\n<div class=\"card-body\">\n<context-aware-component>\n<h4>Dynamic Content</h4>\n<p>Watch this component adapt when its container changes.</p>\n</context-aware-component>\n<div class=\"theme-toggle\">\n<button id=\"toggle-container\">Toggle Container Type</button>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\nCSS\n/* Design System Variables */\n:root {\n/* Light Theme (Default) */\n--color-bg: #f8f9fa;\n--color-surface: #ffffff;\n--color-text: #212529;\n--color-text-secondary: #6c757d;\n--color-border: #dee2e6;\n--color-primary: #0d6efd;\n--color-success: #198754;\n--color-danger: #dc3545;\n--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);\n--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);\n/* For detecting dark mode in the JavaScript */\n--theme-id: \"light\";\ncolor-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n:root {\n/* Dark Theme */\n--color-bg: #212529;\n--color-surface: #343a40;\n--color-text: #f8f9fa;\n--color-text-secondary: #adb5bd;\n--color-border: #495057;\n--color-primary: #0d6efd;\n--color-success: #198754;\n--color-danger: #dc3545;\n--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);\n--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);\n/* For detecting dark mode in the JavaScript */\n--theme-id: \"dark\";\n}\n}\n/* Base Styles */\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\nbackground-color: var(--color-bg);\ncolor: var(--color-text);\nline-height: 1.5;\nmargin: 0;\npadding: 20px;\ntransition: background-color 0.3s ease;\n}\n.container {\nmax-width: 1200px;\nmargin: 0 auto;\n}\nh1,\nh2,\nh3,\nh4 {\ncolor: var(--color-text);\nmargin-top: 0;\n}\np {\ncolor: var(--color-text-secondary);\n}\n.title-section {\nmargin-bottom: 40px;\ntext-align: center;\n}\n.demo-section {\nmargin-bottom: 40px;\npadding: 20px;\nbackground-color: var(--color-surface);\nborder-radius: 8px;\nbox-shadow: var(--shadow-sm);\nborder: 1px solid var(--color-border);\n}\n.card-container {\nmargin-top: 20px;\ndisplay: grid;\ngrid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\ngap: 20px;\n}\n/* Card Styling */\n.card {\nbackground-color: var(--color-surface);\nborder-radius: 8px;\nborder: 1px solid var(--color-border);\noverflow: hidden;\n}\n.card-header {\npadding: 15px;\nbackground-color: rgba(0, 0, 0, 0.03);\nborder-bottom: 1px solid var(--color-border);\n}\n.card-body {\npadding: 15px;\n}\n/* Sidebar Styling */\n.sidebar {\nbackground-color: var(--color-surface);\nborder-radius: 8px;\nborder: 1px solid var(--color-border);\npadding: 15px;\n}\n.sidebar-header {\nborder-bottom: 1px solid var(--color-border);\npadding-bottom: 10px;\nmargin-bottom: 15px;\n}\n.sidebar-items {\ndisplay: flex;\nflex-direction: column;\ngap: 5px;\n}\n.theme-toggle {\nmargin-top: 20px;\ntext-align: center;\n}\nbutton {\nbackground-color: var(--color-primary);\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 14px;\n}\nJavaScript\nclass ContextAwareComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: \"open\" });\n// Render initial component\nthis.render();\n// Initialize MutationObserver\nthis._observer = new MutationObserver(() => this._detectContext());\n}\nconnectedCallback() {\n// Detect context when added to DOM\nthis._detectContext();\nthis._startContextObservation();\n// Listen for theme changes\nwindow.addEventListener(\"theme-change\", () => this._detectContext());\n}\ndisconnectedCallback() {\n// Clean up observer\nthis._observer.disconnect();\nwindow.removeEventListener(\"theme-change\", () => this._detectContext());\n}\n_startContextObservation() {\n// Observe this element for attribute changes\nthis._observer.observe(this, { attributes: true });\n// Walk up the DOM tree to find parent containers that might change\nlet parent = this.parentElement;\nwhile (parent) {\n// If the parent is a custom element or one of our contexts, observe it\nif (\nparent.tagName?.includes(\"-\") ||\nparent.id === \"card-context\" ||\nparent.id === \"sidebar-context\" ||\nparent.id === \"dynamic-container\")\n{\nthis._observer.observe(parent, { attributes: true });\n}\nparent = parent.parentElement;\n}\n}\n_detectContext() {\n// Reset context\nthis.removeAttribute(\"data-context\");\n// Detect container context\nif (this.closest(\"#card-context, .card\")) {\nthis.setAttribute(\"data-context\", \"card\");\n} else if (this.closest(\"#sidebar-context, .sidebar\")) {\nthis.setAttribute(\"data-context\", \"sidebar\");\n}\n// Detect theme context\nconst computedStyle = getComputedStyle(document.documentElement);\nconst themeId = computedStyle.\ngetPropertyValue(\"--theme-id\").\ntrim().\nreplace(/\"/g, \"\");\nthis.setAttribute(\"data-theme\", themeId);\n// Re-render with new context\nthis.render();\n}\nrender() {\nconst context = this.getAttribute(\"data-context\") || \"default\";\nconst theme = this.getAttribute(\"data-theme\") || \"light\";\n// Context-specific styling\nconst contextStyles = {\ndefault: `\npadding: 15px;\nborder-radius: 4px;\nbackground-color: var(--color-surface);\nborder: 1px solid var(--color-border);\n`,\ncard: `\npadding: 20px;\nborder-radius: 8px;\nbackground-color: rgba(0,0,0,0.03);\nborder: 1px solid var(--color-border);\nmargin: 10px 0;\n`,\nsidebar: `\npadding: 10px;\nborder-left: 3px solid var(--color-primary);\nbackground-color: transparent;\nmargin: 5px 0;\n` };\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\ntransition: all 0.3s ease;\n}\n/* Base responsive container that adapts to context */\n.content {\n${contextStyles[context]}\ncolor: var(--color-text);\n/* Inherit typography properties purposefully */\nfont-family: inherit;\nfont-size: inherit;\nline-height: inherit;\n}\n/* Container animations based on context */\n:host([data-context=\"card\"]) .content:hover {\nbox-shadow: var(--shadow-md);\ntransform: translateY(-2px);\n}\n:host([data-context=\"sidebar\"]) .content:hover {\nbackground-color: rgba(13, 110, 253, 0.05);\n}\n/* Theme-specific styling */\n:host([data-theme=\"dark\"]) .content {\nbox-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n}\n:host([data-theme=\"light\"]) .content {\nbox-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n}\n/* Advanced slotted content styling */\n::slotted(h4) {\nmargin-top: 0;\ncolor: var(--color-text);\nfont-size: ${context === \"sidebar\" ? \"16px\" : \"18px\"};\n}\n::slotted(p) {\nmargin-bottom: 0;\ncolor: var(--color-text-secondary);\nfont-size: ${context === \"sidebar\" ? \"14px\" : \"16px\"};\n}\n.context-label {\ndisplay: inline-block;\nfont-size: 12px;\npadding: 2px 6px;\nborder-radius: 4px;\nmargin-bottom: 8px;\nbackground-color: var(--color-primary);\ncolor: white;\n}\n</style>\n<div class=\"content\">\n<div class=\"context-label\">${context} context (${theme} theme)</div>\n<slot></slot>\n</div>\n`;\n}}\n// Register custom element\ncustomElements.define(\"context-aware-component\", ContextAwareComponent);\n// Theme toggle functionality\ndocument.getElementById(\"theme-toggle\").addEventListener(\"click\", () => {\nconst root = document.documentElement;\nconst computedStyle = getComputedStyle(root);\nconst currentTheme = computedStyle.\ngetPropertyValue(\"--theme-id\").\ntrim().\nreplace(/\"/g, \"\");\nif (currentTheme === \"light\") {\nroot.style.setProperty(\"--color-bg\", \"#212529\");\nroot.style.setProperty(\"--color-surface\", \"#343a40\");\nroot.style.setProperty(\"--color-text\", \"#f8f9fa\");\nroot.style.setProperty(\"--color-text-secondary\", \"#adb5bd\");\nroot.style.setProperty(\"--color-border\", \"#495057\");\nroot.style.setProperty(\"--theme-id\", '\"dark\"');\n} else {\nroot.style.setProperty(\"--color-bg\", \"#f8f9fa\");\nroot.style.setProperty(\"--color-surface\", \"#ffffff\");\nroot.style.setProperty(\"--color-text\", \"#212529\");\nroot.style.setProperty(\"--color-text-secondary\", \"#6c757d\");\nroot.style.setProperty(\"--color-border\", \"#dee2e6\");\nroot.style.setProperty(\"--theme-id\", '\"light\"');\n}\n// Dispatch custom event for theme change\nwindow.dispatchEvent(new CustomEvent(\"theme-change\"));\n});\n// Toggle container type\ndocument.getElementById(\"toggle-container\").addEventListener(\"click\", () => {\nconst container = document.getElementById(\"dynamic-container\");\nif (container.classList.contains(\"card\")) {\ncontainer.classList.remove(\"card\");\ncontainer.classList.add(\"sidebar\");\n} else {\ncontainer.classList.remove(\"sidebar\");\ncontainer.classList.add(\"card\");\n}\n});\nContext-Aware Web Components on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\nPerformance Optimization Strategies\nOptimal Web Component styling requires consideration of rendering performance, particularly when designing systems that may include hundreds of component instances.\nCritical Rendering Path Optimization\nclass PerformanceOptimizedComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.shadow = this.attachShadow({ mode: 'open' });\n// Prioritize structural rendering\nthis._createDOMStructure();\n// Defer non-critical styles\nthis._deferNonCriticalStyles();\n}\n_createDOMStructure() {\n// Critical CSS for initial render\nconst criticalStyle = document.createElement('style');\ncriticalStyle.textContent = `\n:host { display: block; }\n.container { min-height: 20px; }\n`;\nconst container = document.createElement('div');\ncontainer.className = 'container';\ncontainer.innerHTML = '<slot></slot>';\nthis.shadow.appendChild(criticalStyle);\nthis.shadow.appendChild(container);\n}\n_deferNonCriticalStyles() {\n// Use requestIdleCallback for non-critical styling\nrequestIdleCallback(() => {\nconst enhancedStyle = document.createElement('style');\nenhancedStyle.textContent = `\n.container {\nbackground-color: var(--bg-color, #f9f9f9);\nborder-radius: var(--radius, 4px);\nbox-shadow: var(--shadow, 0 2px 4px rgba(0,0,0,0.05));\npadding: var(--padding, 16px);\ntransition: transform 150ms ease-out, box-shadow 150ms ease-out;\n}\n.container:hover {\ntransform: translateY(-2px);\nbox-shadow: var(--shadow-hover, 0 4px 8px rgba(0,0,0,0.1));\n}\n::slotted(*) {\nmargin-bottom: var(--item-spacing, 8px);\n}\n::slotted(*:last-child) {\nmargin-bottom: 0;\n}\n`;\nthis.shadow.appendChild(enhancedStyle);\n});\n}\n}\ncustomElements.define('performance-optimized-component', PerformanceOptimizedComponent);\nThis architecture demonstrates several performance optimization techniques:\nPrioritization of critical rendering path styles\nDeferred loading of non-critical styles\nUse of requestIdleCallback for low-priority style enhancements\nMinimal initial render footprint\nPerformance matters when building production-grade Web Components. This demo visualizes the impact of critical rendering path optimization by showing how components can prioritize structural content while deferring non-critical styles. Watch as the component first renders with minimal styling for immediate user interaction, then progressively enhances with advanced styling features. The timeline visualization shows how the component minimizes initial render time while providing a rich interactive experience. Toggle between optimized and unoptimized versions to see the difference in rendering performance.\nHTML\n<div class=\"container\">\n<div class=\"title-section\">\n<h1>Performance Optimization in Web Components</h1>\n<p>Demonstrating critical rendering path optimization techniques for production-ready components</p>\n</div>\n<div class=\"demo-section\">\n<h2>Critical Rendering Path Optimization</h2>\n<p>\nCompare optimized vs. unoptimized components to see how prioritizing critical styles\nand deferring non-essential rendering can dramatically improve perceived performance.\n</p>\n<div class=\"control-panel\">\n<button id=\"load-both\">Load Both Components</button>\n<button id=\"reset-demo\" class=\"secondary\">Reset Demo</button>\n<button id=\"toggle-theme\" class=\"secondary\">Toggle Light/Dark Mode</button>\n</div>\n<div class=\"component-row\">\n<div class=\"component-container\">\n<div class=\"component-label\">Optimized Component</div>\n<div id=\"optimized-container\">\n<p><i>Click \"Load Both Components\" to begin the demo...</i></p>\n</div>\n</div>\n<div class=\"component-container\">\n<div class=\"component-label\">Unoptimized Component</div>\n<div id=\"unoptimized-container\">\n<p><i>Click \"Load Both Components\" to begin the demo...</i></p>\n</div>\n</div>\n</div>\n<div class=\"timeline\">\n<h3>Rendering Timeline</h3>\n<p>Visualizing critical rendering events to demonstrate performance differences</p>\n<div class=\"timeline-container\">\n<div class=\"timeline-track\" id=\"timeline-track\">\n<!-- Timeline markers will be added here -->\n</div>\n<div class=\"timeline-labels\">\n<span>0ms</span>\n<span>500ms</span>\n<span>1000ms</span>\n<span>1500ms</span>\n<span>2000ms</span>\n</div>\n</div>\n<div class=\"stats-panel\">\n<h4>Performance Metrics</h4>\n<div class=\"stats-row\">\n<span>Initial Load (Critical CSS):</span>\n<span id=\"critical-timing\">-</span>\n</div>\n<div class=\"stats-row\">\n<span>Optimized Component Fully Rendered:</span>\n<span id=\"optimized-timing\">-</span>\n</div>\n<div class=\"stats-row\">\n<span>Unoptimized Component Fully Rendered:</span>\n<span id=\"unoptimized-timing\">-</span>\n</div>\n</div>\n</div>\n</div>\n</div>\nCSS\n/* Design System Variables */\n:root {\n/* Light Theme (Default) */\n--color-bg: #f8f9fa;\n--color-surface: #ffffff;\n--color-text: #212529;\n--color-text-secondary: #6c757d;\n--color-border: #dee2e6;\n--color-primary: #0d6efd;\n--color-success: #198754;\n--color-warning: #ffc107;\n--color-danger: #dc3545;\n--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);\n--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);\ncolor-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n:root {\n/* Dark Theme */\n--color-bg: #212529;\n--color-surface: #343a40;\n--color-text: #f8f9fa;\n--color-text-secondary: #adb5bd;\n--color-border: #495057;\n--color-primary: #0d6efd;\n--color-success: #198754;\n--color-warning: #ffc107;\n--color-danger: #dc3545;\n--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);\n--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.3);\n}\n}\n/* Base Styles */\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;\nbackground-color: var(--color-bg);\ncolor: var(--color-text);\nline-height: 1.5;\nmargin: 0;\npadding: 20px;\ntransition: background-color 0.3s ease;\n}\n.container {\nmax-width: 1200px;\nmargin: 0 auto;\n}\nh1,\nh2,\nh3,\nh4 {\ncolor: var(--color-text);\nmargin-top: 0;\n}\np {\ncolor: var(--color-text-secondary);\n}\n.title-section {\nmargin-bottom: 40px;\ntext-align: center;\n}\n.demo-section {\nmargin-bottom: 40px;\npadding: 20px;\nbackground-color: var(--color-surface);\nborder-radius: 8px;\nbox-shadow: var(--shadow-sm);\nborder: 1px solid var(--color-border);\n}\n.control-panel {\ndisplay: flex;\nflex-wrap: wrap;\ngap: 10px;\nmargin-bottom: 20px;\npadding: 15px;\nbackground-color: rgba(0, 0, 0, 0.03);\nborder-radius: 4px;\n}\n.component-row {\ndisplay: grid;\ngrid-template-columns: 1fr 1fr;\ngap: 20px;\nmargin-top: 20px;\n}\n@media (max-width: 768px) {\n.component-row {\ngrid-template-columns: 1fr;\n}\n}\n.component-container {\npadding: 20px;\nborder-radius: 8px;\nborder: 1px solid var(--color-border);\n}\n.timeline {\nmargin-top: 30px;\npadding: 20px;\nbackground-color: var(--color-surface);\nborder-radius: 8px;\nbox-shadow: var(--shadow-sm);\nborder: 1px solid var(--color-border);\n}\n.timeline-container {\nposition: relative;\nheight: 120px;\nmargin-top: 20px;\noverflow: hidden;\n}\n.timeline-track {\nposition: relative;\nheight: 80px;\nbackground-color: rgba(0, 0, 0, 0.05);\nborder-radius: 4px;\n}\n.timeline-marker {\nposition: absolute;\nheight: 100%;\nbackground-color: rgba(13, 110, 253, 0.2);\nborder-left: 2px solid var(--color-primary);\npadding-left: 5px;\ndisplay: flex;\nalign-items: center;\nfont-size: 14px;\ncolor: var(--color-primary);\n}\n.timeline-labels {\ndisplay: flex;\njustify-content: space-between;\nmargin-top: 5px;\nfont-size: 12px;\ncolor: var(--color-text-secondary);\n}\n.timeline-tick {\nposition: absolute;\nwidth: 1px;\nheight: 5px;\nbackground-color: var(--color-text-secondary);\nbottom: -5px;\n}\nbutton {\nbackground-color: var(--color-primary);\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\nfont-size: 14px;\ntransition: background-color 0.2s;\n}\nbutton:hover {\nbackground-color: #0b5ed7;\n}\nbutton.secondary {\nbackground-color: var(--color-surface);\ncolor: var(--color-text);\nborder: 1px solid var(--color-border);\n}\nbutton.secondary:hover {\nbackground-color: rgba(0, 0, 0, 0.05);\n}\n.stats-panel {\nmargin-top: 20px;\npadding: 15px;\nbackground-color: rgba(0, 0, 0, 0.03);\nborder-radius: 4px;\nfont-size: 14px;\n}\n.stats-row {\ndisplay: flex;\njustify-content: space-between;\nmargin-bottom: 5px;\n}\n.theme-toggle {\nmargin-top: 20px;\ntext-align: center;\n}\n.component-label {\nfont-size: 12px;\npadding: 4px 8px;\nbackground-color: rgba(13, 110, 253, 0.1);\ncolor: var(--color-primary);\nborder-radius: 4px;\nmargin-bottom: 10px;\ndisplay: inline-block;\n}\n.spinner {\ndisplay: inline-block;\nwidth: 16px;\nheight: 16px;\nborder: 2px solid rgba(0, 0, 0, 0.1);\nborder-radius: 50%;\nborder-top-color: var(--color-primary);\nanimation: spin 1s linear infinite;\nmargin-right: 10px;\n}\n@keyframes spin {\nto {\ntransform: rotate(360deg);\n}\n}\n@keyframes fadeIn {\nfrom {\nopacity: 0;\n}\nto {\nopacity: 1;\n}\n}\n@keyframes slideIn {\nfrom {\ntransform: translateY(20px);\nopacity: 0;\n}\nto {\ntransform: translateY(0);\nopacity: 1;\n}\n}\nJavaScript\n// Performance Optimized Component\nclass OptimizedComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.shadow = this.attachShadow({ mode: \"open\" });\n// Track timing\nthis.initialRenderTime = null;\nthis.fullRenderTime = null;\n// Log the construction start time\nthis.constructorStartTime = performance.now();\n// STEP 1: Critical Path Rendering - Minimal structural styles\nthis._createCriticalStructure();\n// Record the time for critical path rendering\nthis.initialRenderTime = performance.now() - this.constructorStartTime;\n// STEP 2: Defer non-critical styles\nthis._scheduleDeferredStyles();\n}\nconnectedCallback() {\n// Start the content loading simulation\nthis._simulateContentLoading();\n}\n_createCriticalStructure() {\n// Create and apply only critical CSS for initial render\nconst criticalStyle = document.createElement(\"style\");\ncriticalStyle.textContent = `\n:host {\ndisplay: block;\n}\n.card {\nmin-height: 300px;\nborder: 1px solid var(--color-border, #dee2e6);\nborder-radius: 8px;\noverflow: hidden;\n}\n.card-header {\npadding: 16px;\nbackground-color: rgba(0,0,0,0.03);\nborder-bottom: 1px solid var(--color-border, #dee2e6);\n}\n.card-body {\npadding: 16px;\n}\n.card-title {\nmargin: 0;\nfont-size: 18px;\n}\n.loading {\ndisplay: flex;\nalign-items: center;\nfont-size: 14px;\ncolor: var(--color-text-secondary, #6c757d);\n}\n`;\n// Create minimal structure with content placeholders\nconst card = document.createElement(\"div\");\ncard.className = \"card\";\nconst header = document.createElement(\"div\");\nheader.className = \"card-header\";\nconst title = document.createElement(\"h3\");\ntitle.className = \"card-title\";\ntitle.textContent = \"Optimized Component\";\nconst body = document.createElement(\"div\");\nbody.className = \"card-body\";\nconst loading = document.createElement(\"div\");\nloading.className = \"loading\";\nloading.innerHTML =\n'<div class=\"spinner\"></div> Loading component content...';\n// Assemble the structure\nheader.appendChild(title);\nbody.appendChild(loading);\ncard.appendChild(header);\ncard.appendChild(body);\n// Add to shadow DOM\nthis.shadow.appendChild(criticalStyle);\nthis.shadow.appendChild(card);\n// Store reference to body for content updates\nthis.cardBody = body;\n}\n_scheduleDeferredStyles() {\n// Use requestIdleCallback (or fallback to setTimeout) for non-critical styling\nconst scheduleDeferred =\nwindow.requestIdleCallback || (cb => setTimeout(cb, 200));\nscheduleDeferred(() => {\nthis._applyDeferredStyles();\n});\n}\n_applyDeferredStyles() {\n// Create enhanced styling for the component\nconst enhancedStyle = document.createElement(\"style\");\nenhancedStyle.textContent = `\n.card {\nbox-shadow: var(--shadow-md, 0 4px 6px rgba(0,0,0,0.1));\ntransition: transform 0.3s ease, box-shadow 0.3s ease;\nbackground-color: var(--color-surface, #ffffff);\n}\n.card:hover {\ntransform: translateY(-4px);\nbox-shadow: var(--shadow-lg, 0 10px 15px rgba(0,0,0,0.1));\n}\n.card-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\ntransition: background-color 0.3s ease;\n}\n.card-title {\ncolor: var(--color-primary, #0d6efd);\nfont-weight: 600;\ntransition: color 0.3s ease;\n}\n.card-body {\ncolor: var(--color-text, #212529);\ntransition: color 0.3s ease;\n}\n.card-content {\nanimation: fadeIn 0.5s ease forwards;\n}\n.feature-list {\npadding: 0;\nlist-style-position: inside;\n}\n.feature-item {\npadding: 8px 0;\nborder-bottom: 1px solid var(--color-border, #dee2e6);\ntransition: background-color 0.3s ease;\n}\n.feature-item:last-child {\nborder-bottom: none;\n}\n.card-footer {\npadding: 16px;\nbackground-color: rgba(0,0,0,0.02);\nborder-top: 1px solid var(--color-border, #dee2e6);\ntext-align: center;\n}\n.btn {\ndisplay: inline-block;\npadding: 8px 16px;\nbackground-color: var(--color-primary, #0d6efd);\ncolor: white;\nborder-radius: 4px;\ntext-decoration: none;\ntransition: background-color 0.3s ease;\n}\n.btn:hover {\nbackground-color: #0b5ed7;\n}\n.badge {\ndisplay: inline-block;\npadding: 4px 8px;\nbackground-color: rgba(13, 110, 253, 0.2);\ncolor: var(--color-primary, #0d6efd);\nborder-radius: 20px;\nfont-size: 12px;\n}\n@keyframes fadeIn {\nfrom { opacity: 0; }\nto { opacity: 1; }\n}\n`;\nthis.shadow.appendChild(enhancedStyle);\n}\n_simulateContentLoading() {\n// Simulate content loading delay\nsetTimeout(() => {\n// Replace loading indicator with actual content\nconst content = document.createElement(\"div\");\ncontent.className = \"card-content\";\ncontent.innerHTML = `\n<h4>Performance Optimized Component</h4>\n<p>This component prioritizes the critical rendering path:</p>\n<ul class=\"feature-list\">\n<li class=\"feature-item\">Minimal initial CSS for faster painting</li>\n<li class=\"feature-item\">Deferred non-critical styles using requestIdleCallback</li>\n<li class=\"feature-item\">Progressive enhancement approach</li>\n<li class=\"feature-item\">Optimized animation and transition performance</li>\n</ul>\n<div class=\"card-footer\">\n<span class=\"badge\">Optimized</span>\n<a href=\"#\" class=\"btn\">Learn More</a>\n</div>\n`;\n// Replace loading state with content\nthis.cardBody.innerHTML = \"\";\nthis.cardBody.appendChild(content);\n// Record the time for full rendering\nthis.fullRenderTime = performance.now() - this.constructorStartTime;\n// Dispatch event when component is fully rendered\nthis.dispatchEvent(\nnew CustomEvent(\"component-rendered\", {\nbubbles: true,\ncomposed: true,\ndetail: {\ntype: \"optimized\",\ninitialRender: this.initialRenderTime,\nfullRender: this.fullRenderTime } }));\n}, 800); // Simulate a network delay of 800ms\n}}\n// Unoptimized Component (monolithic approach, loads everything at once)\nclass UnoptimizedComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.shadow = this.attachShadow({ mode: \"open\" });\n// Track timing\nthis.constructorStartTime = performance.now();\n// Apply all styles and structure immediately (unoptimized approach)\nthis._createFullComponent();\n}\nconnectedCallback() {\n// Start the content loading simulation\nthis._simulateContentLoading();\n}\n_createFullComponent() {\n// Create ALL styles upfront - this blocks the initial render\nconst style = document.createElement(\"style\");\nstyle.textContent = `\n:host {\ndisplay: block;\n}\n.card {\nmin-height: 300px;\nborder: 1px solid var(--color-border, #dee2e6);\nborder-radius: 8px;\noverflow: hidden;\nbox-shadow: var(--shadow-md, 0 4px 6px rgba(0,0,0,0.1));\ntransition: transform 0.3s ease, box-shadow 0.3s ease;\nbackground-color: var(--color-surface, #ffffff);\n}\n.card:hover {\ntransform: translateY(-4px);\nbox-shadow: var(--shadow-lg, 0 10px 15px rgba(0,0,0,0.1));\n}\n.card-header {\npadding: 16px;\nbackground-color: rgba(0,0,0,0.03);\nborder-bottom: 1px solid var(--color-border, #dee2e6);\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\ntransition: background-color 0.3s ease;\n}\n.card-title {\nmargin: 0;\nfont-size: 18px;\ncolor: var(--color-primary, #0d6efd);\nfont-weight: 600;\ntransition: color 0.3s ease;\n}\n.card-body {\npadding: 16px;\ncolor: var(--color-text, #212529);\ntransition: color 0.3s ease;\n}\n.loading {\ndisplay: flex;\nalign-items: center;\nfont-size: 14px;\ncolor: var(--color-text-secondary, #6c757d);\n}\n.card-content {\nanimation: fadeIn 0.5s ease forwards;\n}\n.feature-list {\npadding: 0;\nlist-style-position: inside;\n}\n.feature-item {\npadding: 8px 0;\nborder-bottom: 1px solid var(--color-border, #dee2e6);\ntransition: background-color 0.3s ease;\n}\n.feature-item:last-child {\nborder-bottom: none;\n}\n.card-footer {\npadding: 16px;\nbackground-color: rgba(0,0,0,0.02);\nborder-top: 1px solid var(--color-border, #dee2e6);\ntext-align: center;\n}\n.btn {\ndisplay: inline-block;\npadding: 8px 16px;\nbackground-color: var(--color-primary, #0d6efd);\ncolor: white;\nborder-radius: 4px;\ntext-decoration: none;\ntransition: background-color 0.3s ease;\n}\n.btn:hover {\nbackground-color: #0b5ed7;\n}\n.badge {\ndisplay: inline-block;\npadding: 4px 8px;\nbackground-color: rgba(13, 110, 253, 0.2);\ncolor: var(--color-primary, #0d6efd);\nborder-radius: 20px;\nfont-size: 12px;\n}\n@keyframes fadeIn {\nfrom { opacity: 0; }\nto { opacity: 1; }\n}\n/* Extra unnecessary styles to simulate bloat */\n.unused-1 { color: red; }\n.unused-2 { background-color: green; }\n.unused-3 { border: 1px solid blue; }\n.unused-4 { margin: 10px; }\n.unused-5 { padding: 15px; }\n.unused-6 { font-size: 24px; }\n.unused-7 { text-align: center; }\n.unused-8 { display: flex; }\n.unused-9 { position: relative; }\n.unused-10 { width: 100%; }\n`;\n// Create the full structure\nconst card = document.createElement(\"div\");\ncard.className = \"card\";\nconst header = document.createElement(\"div\");\nheader.className = \"card-header\";\nconst title = document.createElement(\"h3\");\ntitle.className = \"card-title\";\ntitle.textContent = \"Unoptimized Component\";\nconst body = document.createElement(\"div\");\nbody.className = \"card-body\";\nconst loading = document.createElement(\"div\");\nloading.className = \"loading\";\nloading.innerHTML =\n'<div class=\"spinner\"></div> Loading component content...';\n// Assemble the structure\nheader.appendChild(title);\nbody.appendChild(loading);\ncard.appendChild(header);\ncard.appendChild(body);\n// Add to shadow DOM (all at once, blocking rendering)\nthis.shadow.appendChild(style);\nthis.shadow.appendChild(card);\n// Store reference to body for content updates\nthis.cardBody = body;\n// Simulate parsing delay with some dummy operations\nthis._simulateHeavyParsing();\n// Record initial render time (which is already delayed due to heavy parsing)\nthis.initialRenderTime = performance.now() - this.constructorStartTime;\n}\n_simulateHeavyParsing() {\n// Simulate intensive parsing of a large component\nconst dummyArray = [];\nfor (let i = 0; i < 10000; i++) {\ndummyArray.push({ id: i, value: `item-${i}` });\n}\n// Sort the array to simulate processing\ndummyArray.sort((a, b) => b.id - a.id);\n// More dummy operations\nlet result = 0;\nfor (let i = 0; i < 5000; i++) {\nresult += Math.sin(i) * Math.cos(i);\n}\n}\n_simulateContentLoading() {\n// Simulate content loading delay (longer than optimized)\nsetTimeout(() => {\n// Replace loading indicator with actual content\nconst content = document.createElement(\"div\");\ncontent.className = \"card-content\";\ncontent.innerHTML = `\n<h4>Unoptimized Component</h4>\n<p>This component loads everything at once:</p>\n<ul class=\"feature-list\">\n<li class=\"feature-item\">All CSS loaded upfront, blocking render</li>\n<li class=\"feature-item\">No prioritization of critical styles</li>\n<li class=\"feature-item\">Contains unused CSS that must be parsed</li>\n<li class=\"feature-item\">No deferred loading strategy</li>\n</ul>\n<div class=\"card-footer\">\n<span class=\"badge\">Unoptimized</span>\n<a href=\"#\" class=\"btn\">Learn More</a>\n</div>\n`;\n// Replace loading state with content\nthis.cardBody.innerHTML = \"\";\nthis.cardBody.appendChild(content);\n// Record the time for full rendering\nthis.fullRenderTime = performance.now() - this.constructorStartTime;\n// Dispatch event when component is fully rendered\nthis.dispatchEvent(\nnew CustomEvent(\"component-rendered\", {\nbubbles: true,\ncomposed: true,\ndetail: {\ntype: \"unoptimized\",\ninitialRender: this.initialRenderTime,\nfullRender: this.fullRenderTime } }));\n}, 1200); // Simulate a longer network delay of 1200ms\n}}\n// Register components\ncustomElements.define(\"optimized-component\", OptimizedComponent);\ncustomElements.define(\"unoptimized-component\", UnoptimizedComponent);\n// Demo Controller\nclass DemoController {\nconstructor() {\nthis.optimizedContainer = document.getElementById(\"optimized-container\");\nthis.unoptimizedContainer = document.getElementById(\"unoptimized-container\");\nthis.timelineTrack = document.getElementById(\"timeline-track\");\nthis.criticalTimingElement = document.getElementById(\"critical-timing\");\nthis.optimizedTimingElement = document.getElementById(\"optimized-timing\");\nthis.unoptimizedTimingElement = document.getElementById(\"unoptimized-timing\");\nthis.startTime = null;\nthis.events = [];\nthis.setupEventListeners();\n}\nsetupEventListeners() {\ndocument.\ngetElementById(\"load-both\").\naddEventListener(\"click\", () => this.loadComponents());\ndocument.\ngetElementById(\"reset-demo\").\naddEventListener(\"click\", () => this.resetDemo());\ndocument.\ngetElementById(\"toggle-theme\").\naddEventListener(\"click\", () => this.toggleTheme());\n// Listen for component render events\ndocument.addEventListener(\"component-rendered\", e => {\nthis.handleComponentRendered(e);\n});\n}\nloadComponents() {\n// Reset the demo first\nthis.resetDemo();\n// Record start time\nthis.startTime = performance.now();\n// Add a \"start\" event to the timeline\nthis.addTimelineEvent(\"Demo Started\", 0, \"#198754\");\n// Create and add the components\nconst optimizedComponent = document.createElement(\"optimized-component\");\nconst unoptimizedComponent = document.createElement(\"unoptimized-component\");\nthis.optimizedContainer.innerHTML = \"\";\nthis.unoptimizedContainer.innerHTML = \"\";\nthis.optimizedContainer.appendChild(optimizedComponent);\nthis.unoptimizedContainer.appendChild(unoptimizedComponent);\n// Disable the load button\ndocument.getElementById(\"load-both\").disabled = true;\n}\nhandleComponentRendered(event) {\nconst { type, initialRender, fullRender } = event.detail;\nconst elapsedTime = performance.now() - this.startTime;\nif (type === \"optimized\") {\n// Add timeline events for optimized component\nthis.addTimelineEvent(\"Optimized Critical Render\", initialRender, \"#0d6efd\");\nthis.addTimelineEvent(\"Optimized Full Render\", fullRender, \"#0d6efd\");\n// Update timing display\nthis.criticalTimingElement.textContent = `${initialRender.toFixed(1)}ms`;\nthis.optimizedTimingElement.textContent = `${fullRender.toFixed(1)}ms`;\n} else if (type === \"unoptimized\") {\n// Add timeline events for unoptimized component\nthis.addTimelineEvent(\n\"Unoptimized Initial Render\",\ninitialRender,\n\"#dc3545\");\nthis.addTimelineEvent(\"Unoptimized Full Render\", fullRender, \"#dc3545\");\n// Update timing display\nthis.unoptimizedTimingElement.textContent = `${fullRender.toFixed(1)}ms`;\n// Re-enable the load button\nsetTimeout(() => {\ndocument.getElementById(\"load-both\").disabled = false;\n}, 500);\n}\n}\naddTimelineEvent(name, time, color) {\n// Calculate position on timeline (scaled to 2000ms max)\nconst position = Math.min(time / 2000, 1) * 100;\n// Create marker element\nconst marker = document.createElement(\"div\");\nmarker.className = \"timeline-marker\";\nmarker.style.left = `${position}%`;\nmarker.style.borderLeftColor = color;\nmarker.style.color = color;\nmarker.textContent = `${name}: ${time.toFixed(1)}ms`;\n// Add to timeline\nthis.timelineTrack.appendChild(marker);\n// Store event\nthis.events.push({\nname,\ntime,\nelement: marker });\n}\nresetDemo() {\n// Clear containers\nthis.optimizedContainer.innerHTML =\n'<p><i>Click \"Load Both Components\" to begin the demo...</i></p>';\nthis.unoptimizedContainer.innerHTML =\n'<p><i>Click \"Load Both Components\" to begin the demo...</i></p>';\n// Clear timeline\nthis.timelineTrack.innerHTML = \"\";\n// Reset timing display\nthis.criticalTimingElement.textContent = \"-\";\nthis.optimizedTimingElement.textContent = \"-\";\nthis.unoptimizedTimingElement.textContent = \"-\";\n// Clear events\nthis.events = [];\n// Re-enable the load button\ndocument.getElementById(\"load-both\").disabled = false;\n}\ntoggleTheme() {\nconst root = document.documentElement;\nconst computedStyle = getComputedStyle(root);\nconst currentBg = computedStyle.getPropertyValue(\"--color-bg\").trim();\n// Check if we're in dark mode\nconst isDark = currentBg === \"#212529\" || currentBg === \"rgb(33, 37, 41)\";\nif (isDark) {\n// Switch to light mode\nroot.style.setProperty(\"--color-bg\", \"#f8f9fa\");\nroot.style.setProperty(\"--color-surface\", \"#ffffff\");\nroot.style.setProperty(\"--color-text\", \"#212529\");\nroot.style.setProperty(\"--color-text-secondary\", \"#6c757d\");\nroot.style.setProperty(\"--color-border\", \"#dee2e6\");\n} else {\n// Switch to dark mode\nroot.style.setProperty(\"--color-bg\", \"#212529\");\nroot.style.setProperty(\"--color-surface\", \"#343a40\");\nroot.style.setProperty(\"--color-text\", \"#f8f9fa\");\nroot.style.setProperty(\"--color-text-secondary\", \"#adb5bd\");\nroot.style.setProperty(\"--color-border\", \"#495057\");\n}\n}}\n// Initialize the demo when the DOM is ready\ndocument.addEventListener(\"DOMContentLoaded\", () => {\nnew DemoController();\n});\nWeb Component Performance Optimization on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/).\nIntegration with Design Systems and Component Libraries\nAdvanced Web Component styling often requires integration with existing design systems. This approach demonstrates a technique for adapting to popular frameworks:\nclass AdaptiveComponent extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['design-system'];\n}\nconstructor() {\nsuper();\nthis.shadow = this.attachShadow({ mode: 'open' });\n// Base structure\nthis.shadow.innerHTML = `\n<div class=\"adaptive-container\">\n<slot></slot>\n</div>\n`;\n// Initialize with default styling\nthis._applyDesignSystem();\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (name === 'design-system' && oldValue !== newValue) {\nthis._applyDesignSystem();\n}\n}\n_applyDesignSystem() {\nconst system = this.getAttribute('design-system') || 'default';\n// Remove previous design system styles\nconst previousStyle = this.shadow.querySelector('.design-system-styles');\nif (previousStyle) {\npreviousStyle.remove();\n}\n// Apply appropriate design system mapping\nconst styleElement = document.createElement('style');\nstyleElement.className = 'design-system-styles';\n// Design system adaptations\nconst designSystemMappings = {\n'default': `\n.adaptive-container {\nfont-family: system-ui, sans-serif;\nbackground-color: #ffffff;\npadding: 16px;\nborder-radius: 4px;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n`,\n'material': `\n.adaptive-container {\nfont-family: Roboto, sans-serif;\nbackground-color: #ffffff;\npadding: 16px;\nborder-radius: 4px;\nbox-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);\n}\n`,\n'tailwind': `\n.adaptive-container {\nfont-family: Inter, system-ui, sans-serif;\nbackground-color: #ffffff;\npadding: 1rem;\nborder-radius: 0.25rem;\nbox-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);\n}\n`\n};\nstyleElement.textContent = designSystemMappings[system] || designSystemMappings.default;\nthis.shadow.prepend(styleElement);\n}\n}\ncustomElements.define('adaptive-component', AdaptiveComponent);\nThis implementation showcases:\nAdaptive styling based on design system context\nDynamic style switching without component reconstruction\nPreservation of component functionality across styling changes\nBest Practices for Production-Grade Web Component Styling\n1. Optimize for Render Performance\nUse CSS containment to isolate rendering paths: contain: content;\nApply critical CSS first, defer non-critical styles\nMinimize style recalculations by batching DOM operations\nUse constructable stylesheets for shared styles\nAvoid deep DOM trees within the shadow root\n2. Design a Clear Styling API\nCreate a consistent naming convention for CSS custom properties\nDocument all customization points in component documentation\nProvide sensible defaults for all custom properties\nUse logical property groups (spacing, colors, typography)\nConsider component variants as entry points for design system integration\n3. Test Across Styling Contexts\nVerify component appearance in light and dark themes\nTest inherited properties like font-family and text color\nEnsure responsive behavior across viewports\nValidate that CSS custom properties fall back gracefully\nVerify that component styling doesn't break when nested\n4. Use Preprocessing for Development Experience\nConsider using SASS/LESS for maintainable component styles\nBuild time CSS custom property optimization\nGenerate documentation from component style definitions\nIntegrate linting tools for consistent styling patterns\nImplement style regression testing for component libraries\nThe Intersection of Styling and Component Lifecycle\nThroughout this article, we've explored sophisticated styling techniques for Web Components, but it's important to recognize that styling doesn't exist in isolation. The component's lifecycle has profound implications for when and how styles are applied, updated, and removed.\nKey lifecycle-related styling considerations include:\nStyle Initialization Timing: Styles defined in the constructor are applied before the component is connected to the DOM, which can impact initial rendering performance.\nDynamic Style Updates: Many components need to update their styles in response to attribute changes, props, or state changes—operations that typically occur during lifecycle events.\nStyle Cleanup: When components are removed from the DOM, any associated stylesheets or external resources should be properly cleaned up to prevent memory leaks.\nHere's how component lifecycle methods relate to styling. In one line each: connectedCallback runs when the element is added to the page, disconnectedCallback when it is removed, and attributeChangedCallback when a watched attribute changes (the lifecycle post (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/) covers all three in depth):\nclass LifecycleAwareStyledComponent extends HTMLElement {\nstatic get observedAttributes() {\nreturn ['theme', 'size', 'disabled'];\n}\nconstructor() {\nsuper();\nthis.shadow = this.attachShadow({ mode: 'open' });\n// Initial styles applied during construction\nthis._createBaseStyles();\n}\nconnectedCallback() {\n// Apply context-dependent styles when added to DOM\nthis._applyContextualStyles();\n// Initialize theme from current document context\nthis._synchronizeWithGlobalTheme();\n// Add event listeners for theme changes\ndocument.addEventListener('theme-changed', this._handleThemeChange);\n}\nattributeChangedCallback(name, oldValue, newValue) {\nif (oldValue === newValue) return;\n// Update styles based on attribute changes\nswitch(name) {\ncase 'theme':\nthis._updateThemeStyles(newValue);\nbreak;\ncase 'size':\nthis._updateSizeStyles(newValue);\nbreak;\ncase 'disabled':\nthis._updateDisabledState(newValue !== null);\nbreak;\n}\n}\ndisconnectedCallback() {\n// Clean up any theme listeners\ndocument.removeEventListener('theme-changed', this._handleThemeChange);\n// Remove any dynamically added stylesheets\n// (important for preventing memory leaks)\nif (this._adoptedStyleSheet) {\nthis.shadow.adoptedStyleSheets =\nthis.shadow.adoptedStyleSheets.filter(sheet => sheet !== this._adoptedStyleSheet);\n}\n}\n// Component-specific implementation of style methods...\n}\nStyling and lifecycle are tied together, so it helps to understand both. The lifecycle methods and component communication are covered in their own post (https://mnixblog.netlify.app/lifecycle-callbacks-and-component-communication/).\nConclusion: Building a Cohesive Component Ecosystem\nPage CSS stops at the shadow boundary, and you choose the doors: :host for the element itself, ::slotted for content the page supplies, custom properties for values the component reads, and ::part for named inner pieces. Everything else here (constructable stylesheets, containment, design tokens) is a way to do those four things at scale.\nThe demo project for this post is on GitHub if you want the full source: Demonstration GitHub Repository (https://github.com/mnichols08/WebComponentStyles) and Demonstration Deployment (https://mnichols08.github.io/WebComponentStyles/).\nStyling decides how a component looks. The next question is whether everyone can use it: with a keyboard, with a screen reader, with focus where they expect it.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-03-16T15:57:08Z",
      "id": "https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/",
      "image": "https://mnixblog.netlify.app/images/mastering-the-art-of-web-component-styling/cover-v4.jpg",
      "summary": "Style a web component from outside through four doors in its wall: :host, ::slotted, CSS custom properties and ::part.",
      "tags": [
        "JavaScript",
        "Web Components",
        "HTML",
        "CSS",
        "Frameworks"
      ],
      "title": "Mastering the Art of Web Component Styling",
      "url": "https://mnixblog.netlify.app/mastering-the-art-of-web-component-styling/"
    },
    {
      "content_html": "<h2 id=\"introduction\">Introduction<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#introduction\" aria-label=\"Link to this section\">#</a></h2>\n<p>Think of a form letter: the wording is printed once, with blanks where each recipient's name goes. A web component wants the same thing, one fixed layout and a few blanks that each page fills in. HTML gives you two elements for this: <code>&lt;template&gt;</code> holds the layout, and <code>&lt;slot&gt;</code> marks a blank.</p>\n<p>A <code>&lt;template&gt;</code> is a layout the browser keeps aside until we ask for a copy, and a <code>&lt;slot&gt;</code> is a labeled hole in that layout where the page's own content shows up.</p>\n<p>You need only a little background. A custom element is a class that <code>extends HTMLElement</code> registered with <code>customElements.define</code>. Its shadow root is a private tree that page CSS cannot reach. The \"light DOM\" is the ordinary markup you write between the component's tags.</p>\n<hr />\n<h2 id=\"the-template-element\">The <code>&lt;template&gt;</code> Element<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#the-template-element\" aria-label=\"Link to this section\">#</a></h2>\n<p>The <code>&lt;template&gt;</code> element allows us to define HTML fragments that are not rendered immediately when the page loads. Instead, these fragments can be instantiated and inserted into the DOM at runtime using JavaScript. This approach promotes code reuse and helps maintain a clean and efficient DOM structure.</p>\n<p><strong>Example:</strong></p>\n<pre><code class=\"language-html\">&lt;template id=\"my-template\"&gt;\n  &lt;style&gt;\n    .message {\n      color: blue;\n      font-size: 20px;\n    }\n  &lt;/style&gt;\n  &lt;div class=\"message\"&gt;Hello, Friend!&lt;/div&gt;\n&lt;/template&gt;\n</code></pre>\n<p>In this example, the <code>&lt;template&gt;</code> element contains a styled <code>&lt;div&gt;</code> that can be reused multiple times within the document.</p>\n<p><strong>Utilizing the Template in JavaScript:</strong></p>\n<pre><code class=\"language-javascript\">// Access the template element\nconst template = document.getElementById('my-template');\n\n// Clone the content of the template\nconst clone = document.importNode(template.content, true);\n\n// Append the cloned content to the body\ndocument.body.appendChild(clone);\n</code></pre>\n<p>Here, the template's content is cloned and appended to the document's body, rendering the defined message. Two details are easy to miss:</p>\n<ul>\n<li><strong>Why <code>.content</code>?</strong> The <code>&lt;template&gt;</code> element itself stays empty and invisible. Its children live inside <code>template.content</code>, a detached fragment the browser never renders, so scripts in it do not run and images in it do not load.</li>\n<li><strong>Why <code>true</code>?</strong> The second argument asks for a deep copy, children included. Leave it out and the failure is a quiet one: you get an empty fragment, and <code>appendChild</code> adds nothing.</li>\n</ul>\n<p><code>template.content.cloneNode(true)</code> does the same job and is what the later examples use.</p>\n<p><strong>Optional:</strong> the next two subsections are about where a template can come from. You can skip to <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#the-slot-element\">The <code>&lt;slot&gt;</code> Element</a> and come back later.</p>\n<p>If we want to load a web component <strong>without embedding the HTML template directly in the file</strong>, we can dynamically fetch the HTML template from an external source (such as a separate HTML file, a server endpoint, or a CDN). This approach is useful for:</p>\n<ol>\n<li>\n<p><strong>Modularity</strong>: Keep our HTML templates separate from our JavaScript logic.</p>\n</li>\n<li>\n<p><strong>Lazy Loading</strong>: Load templates only when needed, improving initial page load performance.</p>\n</li>\n<li>\n<p><strong>Reusability</strong>: Share templates across multiple components or projects.</p>\n</li>\n</ol>\n<p>Here is an interactive demo of cloning a template element like we learned above</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Basic Template Usage</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Basic Template Example&lt;/h1&gt;\n  &lt;p&gt;This example shows how to use the &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; element to define content that isn&#39;t rendered immediately.&lt;/p&gt;\n  \n  &lt;!-- Define a template that won&#39;t be rendered until we use it --&gt;\n  &lt;template id=&quot;card-template&quot;&gt;\n    &lt;div class=&quot;card&quot;&gt;\n      &lt;h2&gt;Card Title&lt;/h2&gt;\n      &lt;p&gt;This content was defined in a template element and then cloned into the document.&lt;/p&gt;\n      &lt;button&gt;Click Me&lt;/button&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n  \n  &lt;button id=&quot;add-card&quot;&gt;Add New Card&lt;/button&gt;\n  &lt;div id=&quot;container&quot;&gt;&lt;/div&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n    .card {\n      border: 1px solid #ddd;\n      border-radius: 8px;\n      padding: 15px;\n      margin-bottom: 15px;\n      background-color: #f8f9fa;\n    }\n    button {\n      background-color: #4CAF50;\n      color: white;\n      border: none;\n      padding: 8px 16px;\n      border-radius: 4px;\n      cursor: pointer;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Get references to our elements\nconst addButton = document.getElementById(&#39;add-card&#39;);\nconst template = document.getElementById(&#39;card-template&#39;);\nconst container = document.getElementById(&#39;container&#39;);\n\n// Add click event to the button\naddButton.addEventListener(&#39;click&#39;, () =&gt; {\n  // Clone the template content\n  const clone = template.content.cloneNode(true);\n\n  // Add a click event to the button inside the cloned template\n  clone.querySelector(&#39;button&#39;).addEventListener(&#39;click&#39;, e =&gt; {\n    alert(&#39;Button in cloned template was clicked!&#39;);\n  });\n\n  // Add the clone to the document\n  container.appendChild(clone);\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<h3 id=\"fetch-the-template-from-an-external-html-file-optional\"><strong>Fetch the Template from an External HTML File</strong> (optional)<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#fetch-the-template-from-an-external-html-file-optional\" aria-label=\"Link to this section\">#</a></h3>\n<p><code>fetch</code> asks a server for a file. The snippet below is a pattern, not something you can paste and run: <code>path/to/template.html</code> is a placeholder for a real file on your own server, and it will fail on a page opened straight from disk. The runnable demo that follows keeps the same idea self-contained.</p>\n<p>We can store our HTML template in a separate file (e.g., <code>template.html</code>) and fetch it using JavaScript.</p>\n<h4 id=\"example\">Example:<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#example\" aria-label=\"Link to this section\">#</a></h4>\n<ul>\n<li>\n<p><code>template.html</code> (external file):</p>\n<pre><code class=\"language-html\">  &lt;template id=\"my-template\"&gt;\n    &lt;div class=\"card\"&gt;\n      &lt;h2&gt;&lt;slot name=\"title\"&gt;Default Title&lt;/slot&gt;&lt;/h2&gt;\n      &lt;p&gt;&lt;slot name=\"content\"&gt;Default content goes here.&lt;/slot&gt;&lt;/p&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n</code></pre>\n</li>\n<li>\n<p><strong>JavaScript for Web Component</strong>:</p>\n<pre><code class=\"language-javascript\">  class MyComponent extends HTMLElement {\n    constructor() {\n      super();\n      this.attachShadow({ mode: 'open' });\n    }\n\n    async connectedCallback() {\n      // Fetch the external template (replace 'path/to/' with the actual path)\n      const response = await fetch('path/to/template.html');\n      const text = await response.text();\n\n      // Parse the fetched HTML and extract the template\n      const parser = new DOMParser();\n      const doc = parser.parseFromString(text, 'text/html');\n      const template = doc.getElementById('my-template').content;\n\n      // Clone and append the template to the shadow DOM\n      this.shadowRoot.appendChild(template.cloneNode(true));\n    }\n  }\n\n  customElements.define('my-component', MyComponent);\n</code></pre>\n</li>\n</ul>\n<p>This example shows how to swap different templates at runtime to completely change a component's appearance while preserving its content.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Dynamically Loading Templates</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Dynamically Loading Templates Example&lt;/h1&gt;\n  &lt;p&gt;This example shows how to dynamically load different templates into a component to change its appearance.&lt;/p&gt;\n  \n  &lt;div class=&quot;theme-selector&quot;&gt;\n    &lt;h3&gt;Select a card theme:&lt;/h3&gt;\n    &lt;button id=&quot;basic-theme&quot; class=&quot;selected&quot;&gt;Basic&lt;/button&gt;\n    &lt;button id=&quot;modern-theme&quot;&gt;Modern&lt;/button&gt;\n    &lt;button id=&quot;elegant-theme&quot;&gt;Elegant&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;!-- External templates --&gt;\n  &lt;template id=&quot;basic-template&quot;&gt;\n    &lt;style&gt;\n      .card {\n        border: 1px solid #ddd;\n        border-radius: 8px;\n        padding: 20px;\n        background-color: #f8f9fa;\n      }\n      \n      .card-header {\n        border-bottom: 1px solid #eee;\n        padding-bottom: 10px;\n        margin-bottom: 10px;\n      }\n      \n      .card-title {\n        margin: 0;\n        color: #333;\n      }\n      \n      .card-body {\n        line-height: 1.5;\n      }\n      \n      .card-footer {\n        margin-top: 15px;\n        padding-top: 15px;\n        border-top: 1px solid #eee;\n        text-align: right;\n      }\n    &lt;/style&gt;\n    \n    &lt;div class=&quot;card&quot;&gt;\n      &lt;div class=&quot;card-header&quot;&gt;\n        &lt;h3 class=&quot;card-title&quot;&gt;&lt;slot name=&quot;title&quot;&gt;Card Title&lt;/slot&gt;&lt;/h3&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;card-body&quot;&gt;\n        &lt;slot&gt;Card content goes here.&lt;/slot&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;card-footer&quot;&gt;\n        &lt;slot name=&quot;footer&quot;&gt;Card footer&lt;/slot&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n  \n  &lt;template id=&quot;modern-template&quot;&gt;\n    &lt;style&gt;\n      .card {\n        border: none;\n        border-radius: 12px;\n        padding: 24px;\n        background-color: #ffffff;\n        box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n      }\n      \n      .card-header {\n        margin-bottom: 16px;\n      }\n      \n      .card-title {\n        margin: 0;\n        color: #1a73e8;\n        font-size: 1.4em;\n      }\n      \n      .card-body {\n        line-height: 1.6;\n        color: #555;\n      }\n      \n      .card-footer {\n        margin-top: 20px;\n        padding-top: 20px;\n        display: flex;\n        justify-content: flex-end;\n      }\n      \n      ::slotted(button) {\n        background-color: #1a73e8;\n        color: white;\n        border: none;\n        padding: 8px 16px;\n        border-radius: 4px;\n        cursor: pointer;\n      }\n    &lt;/style&gt;\n    \n    &lt;div class=&quot;card&quot;&gt;\n      &lt;div class=&quot;card-header&quot;&gt;\n        &lt;h3 class=&quot;card-title&quot;&gt;&lt;slot name=&quot;title&quot;&gt;Card Title&lt;/slot&gt;&lt;/h3&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;card-body&quot;&gt;\n        &lt;slot&gt;Card content goes here.&lt;/slot&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;card-footer&quot;&gt;\n        &lt;slot name=&quot;footer&quot;&gt;Card footer&lt;/slot&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n  \n  &lt;template id=&quot;elegant-template&quot;&gt;\n    &lt;style&gt;\n      .card {\n        border: 1px solid #e0e0e0;\n        border-radius: 0;\n        padding: 30px;\n        background-color: #fff;\n        position: relative;\n        font-family: Georgia, serif;\n      }\n      \n      .card::before {\n        content: &#39;&#39;;\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        height: 5px;\n        background: linear-gradient(to right, #8e44ad, #3498db);\n      }\n      \n      .card-header {\n        margin-bottom: 20px;\n      }\n      \n      .card-title {\n        margin: 0;\n        color: #2c3e50;\n        font-size: 1.5em;\n        font-weight: normal;\n        letter-spacing: 1px;\n      }\n      \n      .card-body {\n        line-height: 1.7;\n        color: #34495e;\n        font-size: 1.1em;\n      }\n      \n      .card-footer {\n        margin-top: 25px;\n        padding-top: 20px;\n        border-top: 1px solid #f0f0f0;\n        font-style: italic;\n        color: #7f8c8d;\n      }\n    &lt;/style&gt;\n    \n    &lt;div class=&quot;card&quot;&gt;\n      &lt;div class=&quot;card-header&quot;&gt;\n        &lt;h3 class=&quot;card-title&quot;&gt;&lt;slot name=&quot;title&quot;&gt;Card Title&lt;/slot&gt;&lt;/h3&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;card-body&quot;&gt;\n        &lt;slot&gt;Card content goes here.&lt;/slot&gt;\n      &lt;/div&gt;\n      &lt;div class=&quot;card-footer&quot;&gt;\n        &lt;slot name=&quot;footer&quot;&gt;Card footer&lt;/slot&gt;\n      &lt;/div&gt;\n    &lt;/div&gt;\n  &lt;/template&gt;\n  \n  &lt;themed-card&gt;\n    &lt;h2 slot=&quot;title&quot;&gt;Understanding Web Components&lt;/h2&gt;\n    &lt;p&gt;Web Components are a set of standards that allow for the creation of reusable custom elements with their functionality encapsulated away from the rest of your code.&lt;/p&gt;\n    &lt;p&gt;They consist of Custom Elements, Shadow DOM, and HTML Templates, which together provide powerful tools for building modular web applications.&lt;/p&gt;\n    &lt;div slot=&quot;footer&quot;&gt;\n      &lt;button&gt;Learn More&lt;/button&gt;\n    &lt;/div&gt;\n  &lt;/themed-card&gt;\n   \n  &lt;!-- Advanced usage section --&gt;\n  &lt;h2 style=&quot;margin-top: 40px;&quot;&gt;Loading External Templates&lt;/h2&gt;\n  &lt;p&gt;In real-world applications, you might want to load templates from external files instead of embedding them in your HTML.&lt;/p&gt;\n  &lt;p&gt;Here&#39;s a simplified example of how you could fetch templates from external sources:&lt;/p&gt;\n  \n  &lt;pre style=&quot;background-color: #f5f5f5; padding: 15px; border-radius: 4px; overflow: auto;&quot;&gt;\nasync function loadExternalTemplate(url) {\n  try {\n    const response = await fetch(url);\n    const text = await response.text();\n    \n    // Parse the HTML into a document\n    const parser = new DOMParser();\n    const doc = parser.parseFromString(text, &#39;text/html&#39;);\n    \n    // Find and return the template\n    return doc.querySelector(&#39;template&#39;);\n  } catch (error) {\n    console.error(&#39;Error loading template:&#39;, error);\n    return null;\n  }\n}\n\n// Usage in a component\nclass ComponentWithExternalTemplate extends HTMLElement {\n  async connectedCallback() {\n    const template = await loadExternalTemplate(&#39;/path/to/template.html&#39;);\n    if (template) {\n      this.shadowRoot.appendChild(template.content.cloneNode(true));\n    }\n  }\n}\n  &lt;/pre&gt;\n  \n  &lt;p&gt;This approach gives you more flexibility in terms of organizing your components and templates, and can help with code maintainability in larger applications.&lt;/p&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n    \n    .theme-selector {\n      margin: 20px 0;\n    }\n    \n    button {\n      padding: 8px 16px;\n      margin-right: 8px;\n      background-color: #4CAF50;\n      color: white;\n      border: none;\n      border-radius: 4px;\n      cursor: pointer;\n    }\n    \n    button.selected {\n      background-color: #2E7D32;\n      outline: 2px solid #2E7D32;\n      outline-offset: 2px;\n    }\n    \n    button:not(.selected) {\n      background-color: #9E9E9E;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our custom element with dynamic template loading\nclass ThemedCard extends HTMLElement {\n  constructor() {\n    super();\n\n    // Create a shadow root\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Set default theme\n    this.currentTheme = &#39;basic&#39;;\n\n    // Load the initial template\n    this.loadTemplate();\n  }\n\n  // Connect event listeners\n  connectedCallback() {\n    // Listen for theme changes\n    document.getElementById(&#39;basic-theme&#39;).addEventListener(&#39;click&#39;, () =&gt; this.changeTheme(&#39;basic&#39;));\n    document.getElementById(&#39;modern-theme&#39;).addEventListener(&#39;click&#39;, () =&gt; this.changeTheme(&#39;modern&#39;));\n    document.getElementById(&#39;elegant-theme&#39;).addEventListener(&#39;click&#39;, () =&gt; this.changeTheme(&#39;elegant&#39;));\n  }\n\n  // Change the theme\n  changeTheme(theme) {\n    // Update the current theme\n    this.currentTheme = theme;\n\n    // Update button states\n    document.querySelectorAll(&#39;.theme-selector button&#39;).forEach(btn =&gt; {\n      btn.classList.remove(&#39;selected&#39;);\n    });\n    document.getElementById(`${theme}-theme`).classList.add(&#39;selected&#39;);\n\n    // Load the new template\n    this.loadTemplate();\n  }\n\n  // Load the template for the current theme\n  loadTemplate() {\n    // Clear the shadow root\n    while (this.shadowRoot.firstChild) {\n      this.shadowRoot.removeChild(this.shadowRoot.firstChild);\n    }\n\n    // Get the template\nconst templateId = `${this.currentTheme}-template`;\n    const template = document.getElementById(templateId);\n\n    if (template) {\n      // Clone the template content\n      const content = template.content.cloneNode(true);\n\n      // Append to shadow DOM\n      this.shadowRoot.appendChild(content);\n\n      // Announce theme change for screen readers\n      const announcement = document.createElement(&#39;div&#39;);\n      announcement.setAttribute(&#39;aria-live&#39;, &#39;polite&#39;);\n      announcement.style.position = &#39;absolute&#39;;\n      announcement.style.clip = &#39;rect(0, 0, 0, 0)&#39;;\n      announcement.textContent = `Theme changed to ${this.currentTheme}`;\n      this.shadowRoot.appendChild(announcement);\n\n      // Remove announcement after it&#39;s been read\n      setTimeout(() =&gt; {\n        this.shadowRoot.removeChild(announcement);\n      }, 3000);\n    } else {\n      console.error(`Template &quot;${templateId}&quot; not found`);\n    }\n  }\n\n  // Disconnect event listeners when element is removed\n  disconnectedCallback() {\n    document.getElementById(&#39;basic-theme&#39;).removeEventListener(&#39;click&#39;, () =&gt; this.changeTheme(&#39;basic&#39;));\n    document.getElementById(&#39;modern-theme&#39;).removeEventListener(&#39;click&#39;, () =&gt; this.changeTheme(&#39;modern&#39;));\n    document.getElementById(&#39;elegant-theme&#39;).removeEventListener(&#39;click&#39;, () =&gt; this.changeTheme(&#39;elegant&#39;));\n  }}\n\n\n// Register our custom element\ncustomElements.define(&#39;themed-card&#39;, ThemedCard);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-note\" role=\"note\"><p class=\"ob-callout-title\"><strong>Note</strong></p>\n<p>You may see <code>&lt;link rel=\"import\"&gt;</code> in old tutorials. HTML Imports were removed from browsers, so do not use them; <code>fetch</code> or a JavaScript module (below) replace them.</p>\n</aside>\n<h3 id=\"use-javascript-modules-and-template-literals-optional\"><strong>Use JavaScript Modules and Template Literals</strong> (optional)<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#use-javascript-modules-and-template-literals-optional\" aria-label=\"Link to this section\">#</a></h3>\n<p>If we prefer not to fetch templates from external files, we can also define them as strings in JavaScript modules.</p>\n<h4 id=\"example-2\">Example:<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#example-2\" aria-label=\"Link to this section\">#</a></h4>\n<ul>\n<li>\n<p><strong>JavaScript Module</strong>:</p>\n<pre><code class=\"language-javascript\">  const templateString = `\n    &lt;template id=\"my-template\"&gt;\n      &lt;div class=\"card\"&gt;\n        &lt;h2&gt;&lt;slot name=\"title\"&gt;Default Title&lt;/slot&gt;&lt;/h2&gt;\n        &lt;p&gt;&lt;slot name=\"content\"&gt;Default content goes here.&lt;/slot&gt;&lt;/p&gt;\n      &lt;/div&gt;\n    &lt;/template&gt;\n  `;\n\n  export function getTemplate() {\n    const parser = new DOMParser();\n    const doc = parser.parseFromString(templateString, 'text/html');\n    return doc.getElementById('my-template').content;\n  }\n</code></pre>\n</li>\n<li>\n<p><strong>Web Component</strong>:</p>\n<pre><code class=\"language-javascript\">  import { getTemplate } from './template.js';\n\n  class MyComponent extends HTMLElement {\n    constructor() {\n      super();\n      const shadowRoot = this.attachShadow({ mode: 'open' });\n      const template = getTemplate();\n      shadowRoot.appendChild(template.cloneNode(true));\n    }\n  }\n\n  customElements.define('my-component', MyComponent);\n</code></pre>\n</li>\n</ul>\n<hr />\n<h2 id=\"the-slot-element\">The <code>&lt;slot&gt;</code> Element<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#the-slot-element\" aria-label=\"Link to this section\">#</a></h2>\n<p>The <code>&lt;slot&gt;</code> element serves as a placeholder within a web component's shadow DOM, allowing us to define where external content (light DOM) should be inserted. This mechanism enables the creation of flexible and customizable components that can accept and display varying content provided by the component's user.</p>\n<p><strong>Example:</strong></p>\n<pre><code class=\"language-html\">&lt;!-- Define the custom element --&gt;\n&lt;template id=\"my-component-template\"&gt;\n  &lt;style&gt;\n    .container {\n      border: 1px solid #ccc;\n      padding: 10px;\n    }\n  &lt;/style&gt;\n  &lt;div class=\"container\"&gt;\n    &lt;slot name=\"header\"&gt;&lt;/slot&gt;\n    &lt;p&gt;This is the main content of the component.&lt;/p&gt;\n    &lt;slot name=\"footer\"&gt;&lt;/slot&gt;\n  &lt;/div&gt;\n&lt;/template&gt;\n\n&lt;script&gt;\n  class MyComponent extends HTMLElement {\n    constructor() {\n      super();\n      const template = document.getElementById('my-component-template').content;\n      const shadowRoot = this.attachShadow({ mode: 'open' });\n      shadowRoot.appendChild(template.cloneNode(true));\n    }\n  }\n\n  customElements.define('my-component', MyComponent);\n&lt;/script&gt;\n\n&lt;!-- Use the custom element with slotted content --&gt;\n&lt;my-component&gt;\n  &lt;h2 slot=\"header\"&gt;Welcome!&lt;/h2&gt;\n  &lt;p slot=\"footer\"&gt;Thanks for visiting.&lt;/p&gt;\n&lt;/my-component&gt;\n</code></pre>\n<p>In this example, the <code>&lt;my-component&gt;</code> element defines slots named \"header\" and \"footer.\" When the component is used, content is assigned to these slots using the <code>slot</code> attribute, enabling dynamic insertion of external content into the component's structure.</p>\n<p>A named slot only takes children whose <code>slot</code> attribute matches. A child with no <code>slot</code> attribute goes to the one unnamed <code>&lt;slot&gt;</code> (the default slot), and if no default slot exists it is simply not shown.</p>\n<section class=\"ob-knowledge-check\" aria-label=\"Knowledge check\">\n<div class=\"ob-quiz\">\n<p class=\"ob-quiz-prompt\"><strong>A component has <code>&lt;slot name=\"title\"&gt;</code> and one unnamed <code>&lt;slot&gt;</code>. Where does a <code>&lt;p&gt;</code> with no <code>slot</code> attribute end up?</strong></p>\n<ol class=\"ob-quiz-choices\" type=\"A\">\n<li>In the <code>title</code> slot</li>\n<li>In the default (unnamed) slot</li>\n<li>Nowhere: every child needs a <code>slot</code> attribute</li>\n</ol>\n<div class=\"ob-quiz-answer\">\n<p>Answer: B. In the default (unnamed) slot</p>\n<p>Children without a <code>slot</code> attribute are assigned to the default slot.</p>\n</div>\n</div>\n</section>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Simple Slots with Shadow DOM</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Simple Slots Example&lt;/h1&gt;\n  &lt;p&gt;This example shows how to use &lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt; elements within Shadow DOM to create customizable components.&lt;/p&gt;\n  \n  &lt;!-- Using our custom element --&gt;\n  &lt;info-card&gt;\n    &lt;h2&gt;Welcome to Slots!&lt;/h2&gt;\n    &lt;p&gt;This content is being projected into the Shadow DOM through a slot.&lt;/p&gt;\n  &lt;/info-card&gt;\n  \n  &lt;info-card&gt;\n    &lt;h2&gt;Another Card Example&lt;/h2&gt;\n    &lt;p&gt;You can reuse the same component with different content.&lt;/p&gt;\n    &lt;p&gt;&lt;strong&gt;Slots make web components truly reusable.&lt;/strong&gt;&lt;/p&gt;\n  &lt;/info-card&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our custom element\nclass InfoCard extends HTMLElement {\n  constructor() {\n    super();\n\n    // Create a shadow root\n    const shadow = this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create the component structure\n    shadow.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              border: 2px solid #2196F3;\n              border-radius: 8px;\n              padding: 16px;\n              margin-bottom: 20px;\n              background-color: #E3F2FD;\n            }\n            \n            .card-content {\n              padding: 10px;\n            }\n          &lt;/style&gt;\n          \n          &lt;div class=&quot;card-content&quot;&gt;\n            &lt;!-- The slot element acts as a placeholder for content --&gt;\n            &lt;slot&gt;&lt;/slot&gt;\n          &lt;/div&gt;\n        `;\n  }}\n\n\n// Register our custom element\ncustomElements.define(&#39;info-card&#39;, InfoCard);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<hr />\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Named Slots Example</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Named Slots Example&lt;/h1&gt;\n  &lt;p&gt;This example shows how to use named slots to position content in specific places within a component.&lt;/p&gt;\n  \n  &lt;!-- Using our custom element with named slots --&gt;\n  &lt;profile-card&gt;\n    &lt;img slot=&quot;avatar&quot; src=&quot;https://via.placeholder.com/100&quot; alt=&quot;Profile avatar&quot;&gt;\n    &lt;h2 slot=&quot;name&quot;&gt;Jane Doe&lt;/h2&gt;\n    &lt;p slot=&quot;description&quot;&gt;Front-end developer specializing in Web Components and modern JavaScript.&lt;/p&gt;\n    &lt;div slot=&quot;contact&quot;&gt;\n      &lt;p&gt;Email: jane@example.com&lt;/p&gt;\n      &lt;p&gt;Twitter: @janedoe&lt;/p&gt;\n    &lt;/div&gt;\n  &lt;/profile-card&gt;\n  \n  &lt;!-- Another instance with different content --&gt;\n  &lt;profile-card&gt;\n    &lt;img slot=&quot;avatar&quot; src=&quot;https://via.placeholder.com/100&quot; alt=&quot;Profile avatar&quot;&gt;\n    &lt;h2 slot=&quot;name&quot;&gt;John Smith&lt;/h2&gt;\n    &lt;p slot=&quot;description&quot;&gt;UX Designer with 5 years of experience creating user-centered designs.&lt;/p&gt;\n    &lt;div slot=&quot;contact&quot;&gt;\n      &lt;p&gt;Email: john@example.com&lt;/p&gt;\n      &lt;p&gt;LinkedIn: johnsmith&lt;/p&gt;\n    &lt;/div&gt;\n  &lt;/profile-card&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our custom element with named slots\nclass ProfileCard extends HTMLElement {\n  constructor() {\n    super();\n\n    // Create a shadow root\n    const shadow = this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create the component structure with named slots\n    shadow.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              border: 1px solid #ccc;\n              border-radius: 8px;\n              padding: 20px;\n              margin-bottom: 20px;\n              background-color: #fff;\n              box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n            }\n            \n            .profile-header {\n              display: flex;\n              align-items: center;\n              margin-bottom: 15px;\n              border-bottom: 1px solid #eee;\n              padding-bottom: 15px;\n            }\n            \n            .avatar-container {\n              margin-right: 20px;\n            }\n            \n            .avatar-container ::slotted(img) {\n              border-radius: 50%;\n              width: 80px;\n              height: 80px;\n              object-fit: cover;\n            }\n            \n            .name-container ::slotted(h2) {\n              margin: 0;\n              color: #333;\n            }\n            \n            .description-container {\n              margin-bottom: 15px;\n            }\n            \n            .contact-container {\n              background-color: #f5f5f5;\n              padding: 10px;\n              border-radius: 4px;\n            }\n          &lt;/style&gt;\n          \n          &lt;div class=&quot;profile-card&quot;&gt;\n            &lt;div class=&quot;profile-header&quot;&gt;\n              &lt;div class=&quot;avatar-container&quot;&gt;\n                &lt;!-- Named slot for the avatar --&gt;\n                &lt;slot name=&quot;avatar&quot;&gt;&lt;/slot&gt;\n              &lt;/div&gt;\n              &lt;div class=&quot;name-container&quot;&gt;\n                &lt;!-- Named slot for the name --&gt;\n                &lt;slot name=&quot;name&quot;&gt;&lt;/slot&gt;\n              &lt;/div&gt;\n            &lt;/div&gt;\n            \n            &lt;div class=&quot;description-container&quot;&gt;\n              &lt;!-- Named slot for the description --&gt;\n              &lt;slot name=&quot;description&quot;&gt;&lt;/slot&gt;\n            &lt;/div&gt;\n            \n            &lt;div class=&quot;contact-container&quot;&gt;\n              &lt;!-- Named slot for contact information --&gt;\n              &lt;slot name=&quot;contact&quot;&gt;&lt;/slot&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        `;\n  }}\n\n\n// Register our custom element\ncustomElements.define(&#39;profile-card&#39;, ProfileCard);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<h2 id=\"combining-template-and-slot-for-dynamic-components\">Combining <code>&lt;template&gt;</code> and <code>&lt;slot&gt;</code> for Dynamic Components<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#combining-template-and-slot-for-dynamic-components\" aria-label=\"Link to this section\">#</a></h2>\n<p>By integrating <code>&lt;template&gt;</code> and <code>&lt;slot&gt;</code>, we can create dynamic and reusable components that maintain a clear separation between structure and content.</p>\n<p><strong>Example:</strong></p>\n<pre><code class=\"language-html\">&lt;template id=\"card-template\"&gt;\n  &lt;style&gt;\n    .card {\n      border: 1px solid #ddd;\n      border-radius: 5px;\n      padding: 15px;\n      box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);\n    }\n  &lt;/style&gt;\n  &lt;div class=\"card\"&gt;\n    &lt;slot name=\"title\"&gt;&lt;/slot&gt;\n    &lt;slot name=\"content\"&gt;&lt;/slot&gt;\n  &lt;/div&gt;\n&lt;/template&gt;\n\n&lt;script&gt;\n  class CardComponent extends HTMLElement {\n    constructor() {\n      super();\n      const template = document.getElementById('card-template').content;\n      const shadowRoot = this.attachShadow({ mode: 'open' });\n      shadowRoot.appendChild(template.cloneNode(true));\n    }\n  }\n\n  customElements.define('card-component', CardComponent);\n&lt;/script&gt;\n\n&lt;!-- Using the card component --&gt;\n&lt;card-component&gt;\n  &lt;h3 slot=\"title\"&gt;Card Title&lt;/h3&gt;\n  &lt;p slot=\"content\"&gt;This is the content of the card.&lt;/p&gt;\n&lt;/card-component&gt;\n</code></pre>\n<p>Here, the <code>CardComponent</code> uses a template to define its structure and styling. The slots \"title\" and \"content\" allow users to insert custom content, making the component reusable and adaptable.</p>\n<p><strong>Challenge: add a footer slot.</strong> Fallback content is what a slot shows when the page gives it nothing. In the block below, the card has no footer. Add a <code>footer</code> slot to the template (the TODO) with the fallback text \"Thanks for reading\", then run it. Expected result: the footer text appears even though the page never supplies one. Then add a <code>&lt;p slot=\"footer\"&gt;Custom footer&lt;/p&gt;</code> inside <code>&lt;fancy-note&gt;</code> and watch it replace the fallback.</p>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Challenge: a footer slot with fallback</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;fancy-note&gt;\n  &lt;h3 slot=&quot;title&quot;&gt;Hello&lt;/h3&gt;\n&lt;/fancy-note&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body { font-family: sans-serif; }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">class FancyNote extends HTMLElement {\n  constructor() {\n    super();\n    const root = this.attachShadow({ mode: &#39;open&#39; });\n    root.innerHTML = `\n      &lt;style&gt;div { border: 1px solid #999; padding: 8px; }&lt;/style&gt;\n      &lt;div&gt;\n        &lt;slot name=&quot;title&quot;&gt;Untitled&lt;/slot&gt;\n        &lt;!-- TODO: add &lt;slot name=&quot;footer&quot;&gt; with fallback text here --&gt;\n      &lt;/div&gt;`;\n  }\n}\ncustomElements.define(&#39;fancy-note&#39;, FancyNote);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<aside class=\"ob-callout ob-callout-tip\" role=\"note\"><p class=\"ob-callout-title\"><strong>Tip</strong></p>\n<p>The fallback is the text between the tags: <code>&lt;slot name=\"footer\"&gt;Thanks for reading&lt;/slot&gt;</code>.</p>\n</aside>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Slot Fallback Content</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Slot Fallback Content Example&lt;/h1&gt;\n  &lt;p&gt;This example shows how to provide default content for slots that isn&#39;t replaced when no content is provided.&lt;/p&gt;\n  \n  &lt;h2&gt;Card with all slots filled&lt;/h2&gt;\n  &lt;notification-card type=&quot;success&quot;&gt;\n    &lt;span slot=&quot;title&quot;&gt;Success!&lt;/span&gt;\n    &lt;span slot=&quot;message&quot;&gt;Your changes have been saved successfully.&lt;/span&gt;\n    &lt;button slot=&quot;action&quot;&gt;View Changes&lt;/button&gt;\n  &lt;/notification-card&gt;\n  \n  &lt;h2&gt;Card with some slots empty (using fallbacks)&lt;/h2&gt;\n  &lt;notification-card type=&quot;warning&quot;&gt;\n    &lt;span slot=&quot;title&quot;&gt;Warning&lt;/span&gt;\n    &lt;!-- No message or action provided, so fallbacks will be used --&gt;\n  &lt;/notification-card&gt;\n  \n  &lt;h2&gt;Card with only type attribute (all fallbacks)&lt;/h2&gt;\n  &lt;notification-card type=&quot;error&quot;&gt;\n    &lt;!-- No slots provided, all fallbacks will be used --&gt;\n  &lt;/notification-card&gt;\n  \n  &lt;h2&gt;Card with no attributes or slots&lt;/h2&gt;\n  &lt;notification-card&gt;\n    &lt;!-- No type attribute or slots, will use all defaults --&gt;\n  &lt;/notification-card&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our custom element with fallback content in slots\nclass NotificationCard extends HTMLElement {\n  constructor() {\n    super();\n\n    // Create a shadow root\n    const shadow = this.attachShadow({ mode: &#39;open&#39; });\n\n    // Get the notification type or use default\n    const type = this.getAttribute(&#39;type&#39;) || &#39;info&#39;;\n\n    // Define the colors based on notification type\n    const colors = {\n      info: { bg: &#39;#E3F2FD&#39;, border: &#39;#2196F3&#39;, icon: &#39;ℹ️&#39; },\n      success: { bg: &#39;#E8F5E9&#39;, border: &#39;#4CAF50&#39;, icon: &#39;✅&#39; },\n      warning: { bg: &#39;#FFF8E1&#39;, border: &#39;#FFC107&#39;, icon: &#39;⚠️&#39; },\n      error: { bg: &#39;#FFEBEE&#39;, border: &#39;#F44336&#39;, icon: &#39;❌&#39; } };\n\n\n    // Get the color scheme for this notification\n    const colorScheme = colors[type] || colors.info;\n\n    // Create the component structure with fallback content\n    shadow.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              margin-bottom: 20px;\n            }\n            \n            .notification {\n              display: flex;\n              padding: 15px;\n              border-left: 4px solid ${colorScheme.border};\n              background-color: ${colorScheme.bg};\n              border-radius: 4px;\n            }\n            \n            .icon {\n              font-size: 24px;\n              margin-right: 15px;\n            }\n            \n            .content {\n              flex: 1;\n            }\n            \n            .title {\n              font-weight: bold;\n              margin-bottom: 5px;\n            }\n            \n            .action-container {\n              display: flex;\n              align-items: center;\n            }\n            \n            button {\n              padding: 8px 12px;\n              background-color: ${colorScheme.border};\n              color: white;\n              border: none;\n              border-radius: 4px;\n              cursor: pointer;\n            }\n          &lt;/style&gt;\n          \n          &lt;div class=&quot;notification&quot;&gt;\n            &lt;div class=&quot;icon&quot;&gt;${colorScheme.icon}&lt;/div&gt;\n            &lt;div class=&quot;content&quot;&gt;\n              &lt;div class=&quot;title&quot;&gt;\n                &lt;!-- Named slot with fallback content --&gt;\n                &lt;slot name=&quot;title&quot;&gt;\n                  ${type.charAt(0).toUpperCase() + type.slice(1)} Notification\n                &lt;/slot&gt;\n              &lt;/div&gt;\n              &lt;div class=&quot;message&quot;&gt;\n                &lt;!-- Named slot with fallback content --&gt;\n                &lt;slot name=&quot;message&quot;&gt;\n                  This is a default ${type} notification message.\n                &lt;/slot&gt;\n              &lt;/div&gt;\n            &lt;/div&gt;\n            &lt;div class=&quot;action-container&quot;&gt;\n              &lt;!-- Named slot with fallback content --&gt;\n              &lt;slot name=&quot;action&quot;&gt;\n                &lt;button&gt;Dismiss&lt;/button&gt;\n              &lt;/slot&gt;\n            &lt;/div&gt;\n          &lt;/div&gt;\n        `;\n  }}\n\n\n// Register our custom element\ncustomElements.define(&#39;notification-card&#39;, NotificationCard);\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Working with Slot Events</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Slot Events Example&lt;/h1&gt;\n  &lt;p&gt;This example demonstrates how to listen for the &lt;code&gt;slotchange&lt;/code&gt; event to react when slot content changes.&lt;/p&gt;\n  \n  &lt;div class=&quot;controls&quot;&gt;\n    &lt;button id=&quot;add-item&quot;&gt;Add Item&lt;/button&gt;\n    &lt;button id=&quot;remove-item&quot;&gt;Remove Item&lt;/button&gt;\n    &lt;button id=&quot;clear-items&quot;&gt;Clear All Items&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;dynamic-list&gt;\n    &lt;li&gt;Initial item 1&lt;/li&gt;\n    &lt;li&gt;Initial item 2&lt;/li&gt;\n  &lt;/dynamic-list&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n    \n    .controls {\n      margin: 20px 0;\n      padding: 10px;\n      background-color: #f5f5f5;\n      border-radius: 4px;\n    }\n    \n    button {\n      padding: 8px 12px;\n      margin-right: 8px;\n      background-color: #4CAF50;\n      color: white;\n      border: none;\n      border-radius: 4px;\n      cursor: pointer;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our custom element with slot event handling\nclass DynamicList extends HTMLElement {\n  constructor() {\n    super();\n\n    // Create a shadow root\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create the component structure\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              border: 1px solid #ddd;\n              border-radius: 8px;\n              padding: 20px;\n              background-color: #fff;\n              box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n            }\n            \n            .list-container {\n              margin: 0;\n              padding: 0;\n            }\n            \n            .list-header {\n              display: flex;\n              justify-content: space-between;\n              align-items: center;\n              border-bottom: 1px solid #eee;\n              padding-bottom: 10px;\n              margin-bottom: 10px;\n            }\n            \n            .count {\n              background-color: #e0e0e0;\n              border-radius: 12px;\n              padding: 2px 8px;\n              font-size: 14px;\n            }\n            \n            ::slotted(li) {\n              padding: 8px 0;\n              border-bottom: 1px solid #f0f0f0;\n              list-style-position: inside;\n            }\n            \n            ::slotted(li:last-child) {\n              border-bottom: none;\n            }\n          &lt;/style&gt;\n          \n          &lt;div class=&quot;list-container&quot;&gt;\n            &lt;div class=&quot;list-header&quot;&gt;\n              &lt;h3&gt;Dynamic List&lt;/h3&gt;\n              &lt;span class=&quot;count&quot;&gt;2 items&lt;/span&gt;\n            &lt;/div&gt;\n            &lt;ul&gt;\n              &lt;slot&gt;&lt;/slot&gt;\n            &lt;/ul&gt;\n          &lt;/div&gt;\n        `;\n\n    // Get references to elements\n    this.slotElement = this.shadowRoot.querySelector(&#39;slot&#39;);\n    this.countDisplay = this.shadowRoot.querySelector(&#39;.count&#39;);\n\n    // Bind methods to this instance\n    this.updateCount = this.updateCount.bind(this);\n\n    // Add slotchange event listener\n    this.slotElement.addEventListener(&#39;slotchange&#39;, this.updateCount);\n  }\n\n  // Called when the element is inserted into the DOM\n  connectedCallback() {\n    // Update count on initial render\n    this.updateCount();\n  }\n\n  // Update the count when slot content changes\n  updateCount() {\n    // Count the actual children of the custom element instead of using assignedElements\n    const count = this.childElementCount;\n    this.countDisplay.textContent = `${count} item${count !== 1 ? &#39;s&#39; : &#39;&#39;}`;\n\n    // Log the current items for demonstration\n    console.log(&#39;List items updated:&#39;, count);\n  }\n\n  // Clean up when element is removed\n  disconnectedCallback() {\n    this.slotElement.removeEventListener(&#39;slotchange&#39;, this.updateCount);\n  }}\n\n\n// Register our custom element\ncustomElements.define(&#39;dynamic-list&#39;, DynamicList);\n\n// Control buttons functionality\ndocument.getElementById(&#39;add-item&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const list = document.querySelector(&#39;dynamic-list&#39;);\n  const newItem = document.createElement(&#39;li&#39;);\n  newItem.textContent = `New item ${list.children.length + 1}`;\n  list.appendChild(newItem);\n  // Force count update in case the slotchange event doesn&#39;t trigger\n  list.updateCount();\n});\n\ndocument.getElementById(&#39;remove-item&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const list = document.querySelector(&#39;dynamic-list&#39;);\n  if (list.lastElementChild) {\n    list.removeChild(list.lastElementChild);\n    // Force count update in case the slotchange event doesn&#39;t trigger\n    list.updateCount();\n  }\n});\n\ndocument.getElementById(&#39;clear-items&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  const list = document.querySelector(&#39;dynamic-list&#39;);\n  while (list.firstChild) {\n    list.removeChild(list.firstChild);\n  }\n  // Force count update in case the slotchange event doesn&#39;t trigger\nlist.updateCount();\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<figure class=\"ob-code-block ob-web-sandbox\"><figcaption class=\"ob-code-title\">Template Inside a Component</figcaption><p class=\"ob-web-sandbox-label\">HTML</p><pre><code class=\"language-html\">&lt;h1&gt;Template Inside a Component Example&lt;/h1&gt;\n  &lt;p&gt;This example shows how to use templates within a web component to create dynamic content based on data.&lt;/p&gt;\n  \n  &lt;div class=&quot;controls&quot;&gt;\n    &lt;button id=&quot;add-task&quot;&gt;Add Random Task&lt;/button&gt;\n    &lt;button id=&quot;complete-task&quot;&gt;Complete Random Task&lt;/button&gt;\n    &lt;button id=&quot;clear-tasks&quot;&gt;Clear All Tasks&lt;/button&gt;\n  &lt;/div&gt;\n  \n  &lt;task-list&gt;&lt;/task-list&gt;\n</code></pre><p class=\"ob-web-sandbox-label\">CSS</p><pre><code class=\"language-css\">body {\n      font-family: system-ui, -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;\n      max-width: 800px;\n      margin: 0 auto;\n      padding: 20px;\n    }\n    \n    .controls {\n      margin: 20px 0;\n      display: flex;\n      gap: 10px;\n      flex-wrap: wrap;\n    }\n    \n    button {\n      padding: 8px 16px;\n      background-color: #4CAF50;\n      color: white;\n      border: none;\n      border-radius: 4px;\n      cursor: pointer;\n    }\n</code></pre><p class=\"ob-web-sandbox-label\">JavaScript</p><pre><code class=\"language-js\">// Define our custom element with internal template\nclass TaskList extends HTMLElement {\n  constructor() {\n    super();\n\n    // Initialize tasks array\n    this.tasks = [\n    { id: 1, title: &#39;Learn about Web Components&#39;, completed: false },\n    { id: 2, title: &#39;Understand Shadow DOM&#39;, completed: false },\n    { id: 3, title: &#39;Master Templates and Slots&#39;, completed: false }];\n\n\n    // Create a shadow root\n    this.attachShadow({ mode: &#39;open&#39; });\n\n    // Create the component structure with an internal template\n    this.shadowRoot.innerHTML = `\n          &lt;style&gt;\n            :host {\n              display: block;\n              border: 1px solid #ddd;\n              border-radius: 8px;\n              padding: 20px;\n              background-color: #fff;\n              box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n            }\n            \n            h3 {\n              margin-top: 0;\n              border-bottom: 1px solid #eee;\n              padding-bottom: 10px;\n            }\n            \n            .task-count {\n              color: #666;\n              font-size: 14px;\n              margin-left: 10px;\n              font-weight: normal;\n            }\n            \n            .empty-state {\n              padding: 20px;\n              text-align: center;\n              color: #888;\n              font-style: italic;\n            }\n            \n            .task {\n              display: flex;\n              align-items: center;\n              padding: 10px 0;\n              border-bottom: 1px solid #f0f0f0;\n            }\n            \n            .task:last-child {\n              border-bottom: none;\n            }\n            \n            .task.completed .task-title {\n              text-decoration: line-through;\n              color: #888;\n            }\n            \n            .checkbox {\n              margin-right: 10px;\n              appearance: none;\n              width: 18px;\n              height: 18px;\n              border: 2px solid #aaa;\n              border-radius: 4px;\n              outline: none;\n              cursor: pointer;\n              position: relative;\n            }\n            \n            .checkbox:checked {\n              background-color: #4CAF50;\n              border-color: #4CAF50;\n            }\n            \n            .checkbox:checked::after {\n              content: &quot;✓&quot;;\n              position: absolute;\n              color: white;\n              font-size: 14px;\n              top: -2px;\n              left: 3px;\n            }\n            \n            .task-title {\n              flex: 1;\n            }\n            \n            .delete-btn {\n              background: none;\n              border: none;\n              color: #f44336;\n              cursor: pointer;\n              font-size: 16px;\n              opacity: 0.7;\n            }\n            \n            .delete-btn:hover {\n              opacity: 1;\n            }\n          &lt;/style&gt;\n          \n          &lt;div class=&quot;task-list-container&quot;&gt;\n            &lt;h3&gt;Task List &lt;span class=&quot;task-count&quot;&gt;(0 tasks)&lt;/span&gt;&lt;/h3&gt;\n            &lt;div class=&quot;tasks-container&quot;&gt;&lt;/div&gt;\n          &lt;/div&gt;\n          \n          &lt;!-- Template for individual task items --&gt;\n          &lt;template id=&quot;task-template&quot;&gt;\n            &lt;div class=&quot;task&quot;&gt;\n              &lt;input type=&quot;checkbox&quot; class=&quot;checkbox&quot;&gt;\n              &lt;span class=&quot;task-title&quot;&gt;&lt;/span&gt;\n              &lt;button class=&quot;delete-btn&quot;&gt;✕&lt;/button&gt;\n            &lt;/div&gt;\n          &lt;/template&gt;\n          \n          &lt;!-- Template for empty state --&gt;\n          &lt;template id=&quot;empty-template&quot;&gt;\n            &lt;div class=&quot;empty-state&quot;&gt;\n              No tasks found. Add a task to get started.\n            &lt;/div&gt;\n          &lt;/template&gt;\n        `;\n\n    // Bind methods\n    this.renderTasks = this.renderTasks.bind(this);\n    this.toggleTask = this.toggleTask.bind(this);\n    this.deleteTask = this.deleteTask.bind(this);\n\n    // Render initial tasks\n    this.renderTasks();\n  }\n\n  // Connect component event listeners\n  connectedCallback() {\n    this.shadowRoot.addEventListener(&#39;click&#39;, e =&gt; {\n      if (e.target.classList.contains(&#39;checkbox&#39;)) {\n        const taskId = parseInt(e.target.dataset.id);\n        this.toggleTask(taskId);\n      } else if (e.target.classList.contains(&#39;delete-btn&#39;)) {\n        const taskId = parseInt(e.target.dataset.id);\n        this.deleteTask(taskId);\n      }\n    });\n  }\n\n  // Add a new task\n  addTask(title) {\n    const newTask = {\n      id: Date.now(), // Use timestamp as unique ID\n      title: title,\n      completed: false };\n\n\n    this.tasks.push(newTask);\n    this.renderTasks();\n    return newTask;\n  }\n\n  // Toggle task completion status\n  toggleTask(taskId) {\n    this.tasks = this.tasks.map(task =&gt; {\n      if (task.id === taskId) {\n        return { ...task, completed: !task.completed };\n      }\n      return task;\n    });\n\n    this.renderTasks();\n  }\n\n  // Delete a task\n  deleteTask(taskId) {\n    this.tasks = this.tasks.filter(task =&gt; task.id !== taskId);\n    this.renderTasks();\n  }\n\n  // Render all tasks using the template\n  renderTasks() {\n    const container = this.shadowRoot.querySelector(&#39;.tasks-container&#39;);\n    const taskTemplate = this.shadowRoot.getElementById(&#39;task-template&#39;);\n    const emptyTemplate = this.shadowRoot.getElementById(&#39;empty-template&#39;);\n\n    // Update task count\n    this.shadowRoot.querySelector(&#39;.task-count&#39;).textContent =\n    `(${this.tasks.length} task${this.tasks.length !== 1 ? &#39;s&#39; : &#39;&#39;})`;\n\n    // Clear container\n    container.innerHTML = &#39;&#39;;\n\n    // Show empty state if no tasks\n    if (this.tasks.length === 0) {\n      const emptyState = emptyTemplate.content.cloneNode(true);\n      container.appendChild(emptyState);\n      return;\n    }\n\n    // Add each task using the template\n    this.tasks.forEach(task =&gt; {\n      const taskElement = taskTemplate.content.cloneNode(true);\n      const checkbox = taskElement.querySelector(&#39;.checkbox&#39;);\n      const title = taskElement.querySelector(&#39;.task-title&#39;);\n      const deleteBtn = taskElement.querySelector(&#39;.delete-btn&#39;);\n\n      // Set task data\n      checkbox.checked = task.completed;\n      checkbox.dataset.id = task.id;\n      title.textContent = task.title;\n      deleteBtn.dataset.id = task.id;\n\n      // Add completed class if needed\n      if (task.completed) {\n        taskElement.querySelector(&#39;.task&#39;).classList.add(&#39;completed&#39;);\n      }\n\n      // Add to container\n      container.appendChild(taskElement);\n    });\n  }}\n\n\n// Register our custom element\ncustomElements.define(&#39;task-list&#39;, TaskList);\n\n// Control buttons functionality\nconst taskList = document.querySelector(&#39;task-list&#39;);\nconst sampleTasks = [\n&#39;Read Web Components documentation&#39;,\n&#39;Build a custom component library&#39;,\n&#39;Create a portfolio website&#39;,\n&#39;Learn about ARIA attributes&#39;,\n&#39;Study Shadow DOM encapsulation&#39;,\n&#39;Practice with CSS custom properties&#39;,\n&#39;Build an accessible component&#39;,\n&#39;Refactor legacy code&#39;,\n&#39;Write unit tests&#39;];\n\n\ndocument.getElementById(&#39;add-task&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  // Pick a random task from sample tasks\n  const randomTask = sampleTasks[Math.floor(Math.random() * sampleTasks.length)];\n  taskList.addTask(randomTask);\n});\n\ndocument.getElementById(&#39;complete-task&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  // Get incomplete tasks\n  const incompleteTasks = taskList.tasks.filter(task =&gt; !task.completed);\n  if (incompleteTasks.length === 0) return;\n\n  // Pick a random incomplete task\n  const randomTask = incompleteTasks[Math.floor(Math.random() * incompleteTasks.length)];\n  taskList.toggleTask(randomTask.id);\n});\n\ndocument.getElementById(&#39;clear-tasks&#39;).addEventListener(&#39;click&#39;, () =&gt; {\n  taskList.tasks = [];\n  taskList.renderTasks();\n});\n</code></pre><p class=\"ob-portable-note\">Open the live preview on <a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/\">the original article</a>.</p></figure>\n<p>These examples showcase the versatility of templates and slots beyond basic usage. <strong>Slots Fallback Content</strong> creates resilient components with sensible defaults, improving developer experience while maintaining visual consistency. <strong>Slot Events</strong> transform components from passive containers into reactive elements that adapt to their content, though remember what <code>slotchange</code> does and does not report: it fires when the set of nodes assigned to a slot changes (and can fire once on first render if the slot starts with content). It does not fire when the text inside an already-slotted element changes. <strong>Templates Inside Components</strong> demonstrate composition at its best, enabling dynamic interfaces that render different structures based on data or user interaction—particularly valuable for complex interfaces that must adapt to changing requirements.</p>\n<hr />\n<h2 id=\"conclusion\">Conclusion<a class=\"ob-heading-anchor\" href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#conclusion\" aria-label=\"Link to this section\">#</a></h2>\n<p>You can now write a component's layout once as a <code>&lt;template&gt;</code>, clone it into a shadow root, and leave labeled <code>&lt;slot&gt;</code> holes that each page fills with its own markup, with fallback text when it fills nothing.</p>\n<p>One limit remains: the layout is fixed, and the page's CSS cannot restyle what lives inside the shadow tree. The next question is how a component should let outside styles in on purpose, which is styling.</p>\n<p class=\"ob-participation\"><a href=\"https://mnixblog.netlify.app/how-templates-and-slots-work/#ob-discussion\">Join the discussion on the original article</a>.</p>\n",
      "content_text": "How Templates and Slots Work\nCanonical: https://mnixblog.netlify.app/how-templates-and-slots-work/\nHow we can enhance Web Components by using reusable structures and dynamic content\nIntroduction\nThink of a form letter: the wording is printed once, with blanks where each recipient's name goes. A web component wants the same thing, one fixed layout and a few blanks that each page fills in. HTML gives you two elements for this: <template> holds the layout, and <slot> marks a blank.\nA <template> is a layout the browser keeps aside until we ask for a copy, and a <slot> is a labeled hole in that layout where the page's own content shows up.\nYou need only a little background. A custom element is a class that extends HTMLElement registered with customElements.define. Its shadow root is a private tree that page CSS cannot reach. The \"light DOM\" is the ordinary markup you write between the component's tags.\nThe <template> Element\nThe <template> element allows us to define HTML fragments that are not rendered immediately when the page loads. Instead, these fragments can be instantiated and inserted into the DOM at runtime using JavaScript. This approach promotes code reuse and helps maintain a clean and efficient DOM structure.\nExample:\n<template id=\"my-template\">\n<style>\n.message {\ncolor: blue;\nfont-size: 20px;\n}\n</style>\n<div class=\"message\">Hello, Friend!</div>\n</template>\nIn this example, the <template> element contains a styled <div> that can be reused multiple times within the document.\nUtilizing the Template in JavaScript:\n// Access the template element\nconst template = document.getElementById('my-template');\n// Clone the content of the template\nconst clone = document.importNode(template.content, true);\n// Append the cloned content to the body\ndocument.body.appendChild(clone);\nHere, the template's content is cloned and appended to the document's body, rendering the defined message. Two details are easy to miss:\nWhy .content? The <template> element itself stays empty and invisible. Its children live inside template.content, a detached fragment the browser never renders, so scripts in it do not run and images in it do not load.\nWhy true? The second argument asks for a deep copy, children included. Leave it out and the failure is a quiet one: you get an empty fragment, and appendChild adds nothing.\ntemplate.content.cloneNode(true) does the same job and is what the later examples use.\nOptional: the next two subsections are about where a template can come from. You can skip to The <slot> Element (https://mnixblog.netlify.app/how-templates-and-slots-work/#the-slot-element) and come back later.\nIf we want to load a web component without embedding the HTML template directly in the file, we can dynamically fetch the HTML template from an external source (such as a separate HTML file, a server endpoint, or a CDN). This approach is useful for:\nModularity: Keep our HTML templates separate from our JavaScript logic.\nLazy Loading: Load templates only when needed, improving initial page load performance.\nReusability: Share templates across multiple components or projects.\nHere is an interactive demo of cloning a template element like we learned above\nHTML\n<h1>Basic Template Example</h1>\n<p>This example shows how to use the <code>&lt;template&gt;</code> element to define content that isn't rendered immediately.</p>\n<!-- Define a template that won't be rendered until we use it -->\n<template id=\"card-template\">\n<div class=\"card\">\n<h2>Card Title</h2>\n<p>This content was defined in a template element and then cloned into the document.</p>\n<button>Click Me</button>\n</div>\n</template>\n<button id=\"add-card\">Add New Card</button>\n<div id=\"container\"></div>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.card {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 15px;\nmargin-bottom: 15px;\nbackground-color: #f8f9fa;\n}\nbutton {\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\n}\nJavaScript\n// Get references to our elements\nconst addButton = document.getElementById('add-card');\nconst template = document.getElementById('card-template');\nconst container = document.getElementById('container');\n// Add click event to the button\naddButton.addEventListener('click', () => {\n// Clone the template content\nconst clone = template.content.cloneNode(true);\n// Add a click event to the button inside the cloned template\nclone.querySelector('button').addEventListener('click', e => {\nalert('Button in cloned template was clicked!');\n});\n// Add the clone to the document\ncontainer.appendChild(clone);\n});\nBasic Template Usage on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nFetch the Template from an External HTML File (optional)\nfetch asks a server for a file. The snippet below is a pattern, not something you can paste and run: path/to/template.html is a placeholder for a real file on your own server, and it will fail on a page opened straight from disk. The runnable demo that follows keeps the same idea self-contained.\nWe can store our HTML template in a separate file (e.g., template.html) and fetch it using JavaScript.\nExample:\ntemplate.html (external file):\n<template id=\"my-template\">\n<div class=\"card\">\n<h2><slot name=\"title\">Default Title</slot></h2>\n<p><slot name=\"content\">Default content goes here.</slot></p>\n</div>\n</template>\nJavaScript for Web Component:\nclass MyComponent extends HTMLElement {\nconstructor() {\nsuper();\nthis.attachShadow({ mode: 'open' });\n}\nasync connectedCallback() {\n// Fetch the external template (replace 'path/to/' with the actual path)\nconst response = await fetch('path/to/template.html');\nconst text = await response.text();\n// Parse the fetched HTML and extract the template\nconst parser = new DOMParser();\nconst doc = parser.parseFromString(text, 'text/html');\nconst template = doc.getElementById('my-template').content;\n// Clone and append the template to the shadow DOM\nthis.shadowRoot.appendChild(template.cloneNode(true));\n}\n}\ncustomElements.define('my-component', MyComponent);\nThis example shows how to swap different templates at runtime to completely change a component's appearance while preserving its content.\nHTML\n<h1>Dynamically Loading Templates Example</h1>\n<p>This example shows how to dynamically load different templates into a component to change its appearance.</p>\n<div class=\"theme-selector\">\n<h3>Select a card theme:</h3>\n<button id=\"basic-theme\" class=\"selected\">Basic</button>\n<button id=\"modern-theme\">Modern</button>\n<button id=\"elegant-theme\">Elegant</button>\n</div>\n<!-- External templates -->\n<template id=\"basic-template\">\n<style>\n.card {\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 20px;\nbackground-color: #f8f9fa;\n}\n.card-header {\nborder-bottom: 1px solid #eee;\npadding-bottom: 10px;\nmargin-bottom: 10px;\n}\n.card-title {\nmargin: 0;\ncolor: #333;\n}\n.card-body {\nline-height: 1.5;\n}\n.card-footer {\nmargin-top: 15px;\npadding-top: 15px;\nborder-top: 1px solid #eee;\ntext-align: right;\n}\n</style>\n<div class=\"card\">\n<div class=\"card-header\">\n<h3 class=\"card-title\"><slot name=\"title\">Card Title</slot></h3>\n</div>\n<div class=\"card-body\">\n<slot>Card content goes here.</slot>\n</div>\n<div class=\"card-footer\">\n<slot name=\"footer\">Card footer</slot>\n</div>\n</div>\n</template>\n<template id=\"modern-template\">\n<style>\n.card {\nborder: none;\nborder-radius: 12px;\npadding: 24px;\nbackground-color: #ffffff;\nbox-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n}\n.card-header {\nmargin-bottom: 16px;\n}\n.card-title {\nmargin: 0;\ncolor: #1a73e8;\nfont-size: 1.4em;\n}\n.card-body {\nline-height: 1.6;\ncolor: #555;\n}\n.card-footer {\nmargin-top: 20px;\npadding-top: 20px;\ndisplay: flex;\njustify-content: flex-end;\n}\n::slotted(button) {\nbackground-color: #1a73e8;\ncolor: white;\nborder: none;\npadding: 8px 16px;\nborder-radius: 4px;\ncursor: pointer;\n}\n</style>\n<div class=\"card\">\n<div class=\"card-header\">\n<h3 class=\"card-title\"><slot name=\"title\">Card Title</slot></h3>\n</div>\n<div class=\"card-body\">\n<slot>Card content goes here.</slot>\n</div>\n<div class=\"card-footer\">\n<slot name=\"footer\">Card footer</slot>\n</div>\n</div>\n</template>\n<template id=\"elegant-template\">\n<style>\n.card {\nborder: 1px solid #e0e0e0;\nborder-radius: 0;\npadding: 30px;\nbackground-color: #fff;\nposition: relative;\nfont-family: Georgia, serif;\n}\n.card::before {\ncontent: '';\nposition: absolute;\ntop: 0;\nleft: 0;\nright: 0;\nheight: 5px;\nbackground: linear-gradient(to right, #8e44ad, #3498db);\n}\n.card-header {\nmargin-bottom: 20px;\n}\n.card-title {\nmargin: 0;\ncolor: #2c3e50;\nfont-size: 1.5em;\nfont-weight: normal;\nletter-spacing: 1px;\n}\n.card-body {\nline-height: 1.7;\ncolor: #34495e;\nfont-size: 1.1em;\n}\n.card-footer {\nmargin-top: 25px;\npadding-top: 20px;\nborder-top: 1px solid #f0f0f0;\nfont-style: italic;\ncolor: #7f8c8d;\n}\n</style>\n<div class=\"card\">\n<div class=\"card-header\">\n<h3 class=\"card-title\"><slot name=\"title\">Card Title</slot></h3>\n</div>\n<div class=\"card-body\">\n<slot>Card content goes here.</slot>\n</div>\n<div class=\"card-footer\">\n<slot name=\"footer\">Card footer</slot>\n</div>\n</div>\n</template>\n<themed-card>\n<h2 slot=\"title\">Understanding Web Components</h2>\n<p>Web Components are a set of standards that allow for the creation of reusable custom elements with their functionality encapsulated away from the rest of your code.</p>\n<p>They consist of Custom Elements, Shadow DOM, and HTML Templates, which together provide powerful tools for building modular web applications.</p>\n<div slot=\"footer\">\n<button>Learn More</button>\n</div>\n</themed-card>\n<!-- Advanced usage section -->\n<h2 style=\"margin-top: 40px;\">Loading External Templates</h2>\n<p>In real-world applications, you might want to load templates from external files instead of embedding them in your HTML.</p>\n<p>Here's a simplified example of how you could fetch templates from external sources:</p>\n<pre style=\"background-color: #f5f5f5; padding: 15px; border-radius: 4px; overflow: auto;\">\nasync function loadExternalTemplate(url) {\ntry {\nconst response = await fetch(url);\nconst text = await response.text();\n// Parse the HTML into a document\nconst parser = new DOMParser();\nconst doc = parser.parseFromString(text, 'text/html');\n// Find and return the template\nreturn doc.querySelector('template');\n} catch (error) {\nconsole.error('Error loading template:', error);\nreturn null;\n}\n}\n// Usage in a component\nclass ComponentWithExternalTemplate extends HTMLElement {\nasync connectedCallback() {\nconst template = await loadExternalTemplate('/path/to/template.html');\nif (template) {\nthis.shadowRoot.appendChild(template.content.cloneNode(true));\n}\n}\n}\n</pre>\n<p>This approach gives you more flexibility in terms of organizing your components and templates, and can help with code maintainability in larger applications.</p>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.theme-selector {\nmargin: 20px 0;\n}\nbutton {\npadding: 8px 16px;\nmargin-right: 8px;\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\nbutton.selected {\nbackground-color: #2E7D32;\noutline: 2px solid #2E7D32;\noutline-offset: 2px;\n}\nbutton:not(.selected) {\nbackground-color: #9E9E9E;\n}\nJavaScript\n// Define our custom element with dynamic template loading\nclass ThemedCard extends HTMLElement {\nconstructor() {\nsuper();\n// Create a shadow root\nthis.attachShadow({ mode: 'open' });\n// Set default theme\nthis.currentTheme = 'basic';\n// Load the initial template\nthis.loadTemplate();\n}\n// Connect event listeners\nconnectedCallback() {\n// Listen for theme changes\ndocument.getElementById('basic-theme').addEventListener('click', () => this.changeTheme('basic'));\ndocument.getElementById('modern-theme').addEventListener('click', () => this.changeTheme('modern'));\ndocument.getElementById('elegant-theme').addEventListener('click', () => this.changeTheme('elegant'));\n}\n// Change the theme\nchangeTheme(theme) {\n// Update the current theme\nthis.currentTheme = theme;\n// Update button states\ndocument.querySelectorAll('.theme-selector button').forEach(btn => {\nbtn.classList.remove('selected');\n});\ndocument.getElementById(`${theme}-theme`).classList.add('selected');\n// Load the new template\nthis.loadTemplate();\n}\n// Load the template for the current theme\nloadTemplate() {\n// Clear the shadow root\nwhile (this.shadowRoot.firstChild) {\nthis.shadowRoot.removeChild(this.shadowRoot.firstChild);\n}\n// Get the template\nconst templateId = `${this.currentTheme}-template`;\nconst template = document.getElementById(templateId);\nif (template) {\n// Clone the template content\nconst content = template.content.cloneNode(true);\n// Append to shadow DOM\nthis.shadowRoot.appendChild(content);\n// Announce theme change for screen readers\nconst announcement = document.createElement('div');\nannouncement.setAttribute('aria-live', 'polite');\nannouncement.style.position = 'absolute';\nannouncement.style.clip = 'rect(0, 0, 0, 0)';\nannouncement.textContent = `Theme changed to ${this.currentTheme}`;\nthis.shadowRoot.appendChild(announcement);\n// Remove announcement after it's been read\nsetTimeout(() => {\nthis.shadowRoot.removeChild(announcement);\n}, 3000);\n} else {\nconsole.error(`Template \"${templateId}\" not found`);\n}\n}\n// Disconnect event listeners when element is removed\ndisconnectedCallback() {\ndocument.getElementById('basic-theme').removeEventListener('click', () => this.changeTheme('basic'));\ndocument.getElementById('modern-theme').removeEventListener('click', () => this.changeTheme('modern'));\ndocument.getElementById('elegant-theme').removeEventListener('click', () => this.changeTheme('elegant'));\n}}\n// Register our custom element\ncustomElements.define('themed-card', ThemedCard);\nDynamically Loading Templates on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nNote:\nYou may see <link rel=\"import\"> in old tutorials. HTML Imports were removed from browsers, so do not use them; fetch or a JavaScript module (below) replace them.\nUse JavaScript Modules and Template Literals (optional)\nIf we prefer not to fetch templates from external files, we can also define them as strings in JavaScript modules.\nExample:\nJavaScript Module:\nconst templateString = `\n<template id=\"my-template\">\n<div class=\"card\">\n<h2><slot name=\"title\">Default Title</slot></h2>\n<p><slot name=\"content\">Default content goes here.</slot></p>\n</div>\n</template>\n`;\nexport function getTemplate() {\nconst parser = new DOMParser();\nconst doc = parser.parseFromString(templateString, 'text/html');\nreturn doc.getElementById('my-template').content;\n}\nWeb Component:\nimport { getTemplate } from './template.js';\nclass MyComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst shadowRoot = this.attachShadow({ mode: 'open' });\nconst template = getTemplate();\nshadowRoot.appendChild(template.cloneNode(true));\n}\n}\ncustomElements.define('my-component', MyComponent);\nThe <slot> Element\nThe <slot> element serves as a placeholder within a web component's shadow DOM, allowing us to define where external content (light DOM) should be inserted. This mechanism enables the creation of flexible and customizable components that can accept and display varying content provided by the component's user.\nExample:\n<!-- Define the custom element -->\n<template id=\"my-component-template\">\n<style>\n.container {\nborder: 1px solid #ccc;\npadding: 10px;\n}\n</style>\n<div class=\"container\">\n<slot name=\"header\"></slot>\n<p>This is the main content of the component.</p>\n<slot name=\"footer\"></slot>\n</div>\n</template>\n<script>\nclass MyComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst template = document.getElementById('my-component-template').content;\nconst shadowRoot = this.attachShadow({ mode: 'open' });\nshadowRoot.appendChild(template.cloneNode(true));\n}\n}\ncustomElements.define('my-component', MyComponent);\n</script>\n<!-- Use the custom element with slotted content -->\n<my-component>\n<h2 slot=\"header\">Welcome!</h2>\n<p slot=\"footer\">Thanks for visiting.</p>\n</my-component>\nIn this example, the <my-component> element defines slots named \"header\" and \"footer.\" When the component is used, content is assigned to these slots using the slot attribute, enabling dynamic insertion of external content into the component's structure.\nA named slot only takes children whose slot attribute matches. A child with no slot attribute goes to the one unnamed <slot> (the default slot), and if no default slot exists it is simply not shown.\nQuestion: A component has <slot name=\"title\"> and one unnamed <slot>. Where does a <p> with no slot attribute end up?\nIn the title slot\nIn the default (unnamed) slot\nNowhere: every child needs a slot attribute\nAnswer: 2. In the default (unnamed) slot\nExplanation: Children without a slot attribute are assigned to the default slot.\nHTML\n<h1>Simple Slots Example</h1>\n<p>This example shows how to use <code>&lt;slot&gt;</code> elements within Shadow DOM to create customizable components.</p>\n<!-- Using our custom element -->\n<info-card>\n<h2>Welcome to Slots!</h2>\n<p>This content is being projected into the Shadow DOM through a slot.</p>\n</info-card>\n<info-card>\n<h2>Another Card Example</h2>\n<p>You can reuse the same component with different content.</p>\n<p><strong>Slots make web components truly reusable.</strong></p>\n</info-card>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\nJavaScript\n// Define our custom element\nclass InfoCard extends HTMLElement {\nconstructor() {\nsuper();\n// Create a shadow root\nconst shadow = this.attachShadow({ mode: 'open' });\n// Create the component structure\nshadow.innerHTML = `\n<style>\n:host {\ndisplay: block;\nborder: 2px solid #2196F3;\nborder-radius: 8px;\npadding: 16px;\nmargin-bottom: 20px;\nbackground-color: #E3F2FD;\n}\n.card-content {\npadding: 10px;\n}\n</style>\n<div class=\"card-content\">\n<!-- The slot element acts as a placeholder for content -->\n<slot></slot>\n</div>\n`;\n}}\n// Register our custom element\ncustomElements.define('info-card', InfoCard);\nSimple Slots with Shadow DOM on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nHTML\n<h1>Named Slots Example</h1>\n<p>This example shows how to use named slots to position content in specific places within a component.</p>\n<!-- Using our custom element with named slots -->\n<profile-card>\n<img slot=\"avatar\" src=\"https://via.placeholder.com/100\" alt=\"Profile avatar\">\n<h2 slot=\"name\">Jane Doe</h2>\n<p slot=\"description\">Front-end developer specializing in Web Components and modern JavaScript.</p>\n<div slot=\"contact\">\n<p>Email: jane@example.com</p>\n<p>Twitter: @janedoe</p>\n</div>\n</profile-card>\n<!-- Another instance with different content -->\n<profile-card>\n<img slot=\"avatar\" src=\"https://via.placeholder.com/100\" alt=\"Profile avatar\">\n<h2 slot=\"name\">John Smith</h2>\n<p slot=\"description\">UX Designer with 5 years of experience creating user-centered designs.</p>\n<div slot=\"contact\">\n<p>Email: john@example.com</p>\n<p>LinkedIn: johnsmith</p>\n</div>\n</profile-card>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\nJavaScript\n// Define our custom element with named slots\nclass ProfileCard extends HTMLElement {\nconstructor() {\nsuper();\n// Create a shadow root\nconst shadow = this.attachShadow({ mode: 'open' });\n// Create the component structure with named slots\nshadow.innerHTML = `\n<style>\n:host {\ndisplay: block;\nborder: 1px solid #ccc;\nborder-radius: 8px;\npadding: 20px;\nmargin-bottom: 20px;\nbackground-color: #fff;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n.profile-header {\ndisplay: flex;\nalign-items: center;\nmargin-bottom: 15px;\nborder-bottom: 1px solid #eee;\npadding-bottom: 15px;\n}\n.avatar-container {\nmargin-right: 20px;\n}\n.avatar-container ::slotted(img) {\nborder-radius: 50%;\nwidth: 80px;\nheight: 80px;\nobject-fit: cover;\n}\n.name-container ::slotted(h2) {\nmargin: 0;\ncolor: #333;\n}\n.description-container {\nmargin-bottom: 15px;\n}\n.contact-container {\nbackground-color: #f5f5f5;\npadding: 10px;\nborder-radius: 4px;\n}\n</style>\n<div class=\"profile-card\">\n<div class=\"profile-header\">\n<div class=\"avatar-container\">\n<!-- Named slot for the avatar -->\n<slot name=\"avatar\"></slot>\n</div>\n<div class=\"name-container\">\n<!-- Named slot for the name -->\n<slot name=\"name\"></slot>\n</div>\n</div>\n<div class=\"description-container\">\n<!-- Named slot for the description -->\n<slot name=\"description\"></slot>\n</div>\n<div class=\"contact-container\">\n<!-- Named slot for contact information -->\n<slot name=\"contact\"></slot>\n</div>\n</div>\n`;\n}}\n// Register our custom element\ncustomElements.define('profile-card', ProfileCard);\nNamed Slots Example on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nCombining <template> and <slot> for Dynamic Components\nBy integrating <template> and <slot>, we can create dynamic and reusable components that maintain a clear separation between structure and content.\nExample:\n<template id=\"card-template\">\n<style>\n.card {\nborder: 1px solid #ddd;\nborder-radius: 5px;\npadding: 15px;\nbox-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);\n}\n</style>\n<div class=\"card\">\n<slot name=\"title\"></slot>\n<slot name=\"content\"></slot>\n</div>\n</template>\n<script>\nclass CardComponent extends HTMLElement {\nconstructor() {\nsuper();\nconst template = document.getElementById('card-template').content;\nconst shadowRoot = this.attachShadow({ mode: 'open' });\nshadowRoot.appendChild(template.cloneNode(true));\n}\n}\ncustomElements.define('card-component', CardComponent);\n</script>\n<!-- Using the card component -->\n<card-component>\n<h3 slot=\"title\">Card Title</h3>\n<p slot=\"content\">This is the content of the card.</p>\n</card-component>\nHere, the CardComponent uses a template to define its structure and styling. The slots \"title\" and \"content\" allow users to insert custom content, making the component reusable and adaptable.\nChallenge: add a footer slot. Fallback content is what a slot shows when the page gives it nothing. In the block below, the card has no footer. Add a footer slot to the template (the TODO) with the fallback text \"Thanks for reading\", then run it. Expected result: the footer text appears even though the page never supplies one. Then add a <p slot=\"footer\">Custom footer</p> inside <fancy-note> and watch it replace the fallback.\nHTML\n<fancy-note>\n<h3 slot=\"title\">Hello</h3>\n</fancy-note>\nCSS\nbody { font-family: sans-serif; }\nJavaScript\nclass FancyNote extends HTMLElement {\nconstructor() {\nsuper();\nconst root = this.attachShadow({ mode: 'open' });\nroot.innerHTML = `\n<style>div { border: 1px solid #999; padding: 8px; }</style>\n<div>\n<slot name=\"title\">Untitled</slot>\n<!-- TODO: add <slot name=\"footer\"> with fallback text here -->\n</div>`;\n}\n}\ncustomElements.define('fancy-note', FancyNote);\nChallenge: a footer slot with fallback on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nTip:\nThe fallback is the text between the tags: <slot name=\"footer\">Thanks for reading</slot>.\nHTML\n<h1>Slot Fallback Content Example</h1>\n<p>This example shows how to provide default content for slots that isn't replaced when no content is provided.</p>\n<h2>Card with all slots filled</h2>\n<notification-card type=\"success\">\n<span slot=\"title\">Success!</span>\n<span slot=\"message\">Your changes have been saved successfully.</span>\n<button slot=\"action\">View Changes</button>\n</notification-card>\n<h2>Card with some slots empty (using fallbacks)</h2>\n<notification-card type=\"warning\">\n<span slot=\"title\">Warning</span>\n<!-- No message or action provided, so fallbacks will be used -->\n</notification-card>\n<h2>Card with only type attribute (all fallbacks)</h2>\n<notification-card type=\"error\">\n<!-- No slots provided, all fallbacks will be used -->\n</notification-card>\n<h2>Card with no attributes or slots</h2>\n<notification-card>\n<!-- No type attribute or slots, will use all defaults -->\n</notification-card>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\nJavaScript\n// Define our custom element with fallback content in slots\nclass NotificationCard extends HTMLElement {\nconstructor() {\nsuper();\n// Create a shadow root\nconst shadow = this.attachShadow({ mode: 'open' });\n// Get the notification type or use default\nconst type = this.getAttribute('type') || 'info';\n// Define the colors based on notification type\nconst colors = {\ninfo: { bg: '#E3F2FD', border: '#2196F3', icon: 'ℹ️' },\nsuccess: { bg: '#E8F5E9', border: '#4CAF50', icon: '✅' },\nwarning: { bg: '#FFF8E1', border: '#FFC107', icon: '⚠️' },\nerror: { bg: '#FFEBEE', border: '#F44336', icon: '❌' } };\n// Get the color scheme for this notification\nconst colorScheme = colors[type] || colors.info;\n// Create the component structure with fallback content\nshadow.innerHTML = `\n<style>\n:host {\ndisplay: block;\nmargin-bottom: 20px;\n}\n.notification {\ndisplay: flex;\npadding: 15px;\nborder-left: 4px solid ${colorScheme.border};\nbackground-color: ${colorScheme.bg};\nborder-radius: 4px;\n}\n.icon {\nfont-size: 24px;\nmargin-right: 15px;\n}\n.content {\nflex: 1;\n}\n.title {\nfont-weight: bold;\nmargin-bottom: 5px;\n}\n.action-container {\ndisplay: flex;\nalign-items: center;\n}\nbutton {\npadding: 8px 12px;\nbackground-color: ${colorScheme.border};\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\n</style>\n<div class=\"notification\">\n<div class=\"icon\">${colorScheme.icon}</div>\n<div class=\"content\">\n<div class=\"title\">\n<!-- Named slot with fallback content -->\n<slot name=\"title\">\n${type.charAt(0).toUpperCase() + type.slice(1)} Notification\n</slot>\n</div>\n<div class=\"message\">\n<!-- Named slot with fallback content -->\n<slot name=\"message\">\nThis is a default ${type} notification message.\n</slot>\n</div>\n</div>\n<div class=\"action-container\">\n<!-- Named slot with fallback content -->\n<slot name=\"action\">\n<button>Dismiss</button>\n</slot>\n</div>\n</div>\n`;\n}}\n// Register our custom element\ncustomElements.define('notification-card', NotificationCard);\nSlot Fallback Content on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nHTML\n<h1>Slot Events Example</h1>\n<p>This example demonstrates how to listen for the <code>slotchange</code> event to react when slot content changes.</p>\n<div class=\"controls\">\n<button id=\"add-item\">Add Item</button>\n<button id=\"remove-item\">Remove Item</button>\n<button id=\"clear-items\">Clear All Items</button>\n</div>\n<dynamic-list>\n<li>Initial item 1</li>\n<li>Initial item 2</li>\n</dynamic-list>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.controls {\nmargin: 20px 0;\npadding: 10px;\nbackground-color: #f5f5f5;\nborder-radius: 4px;\n}\nbutton {\npadding: 8px 12px;\nmargin-right: 8px;\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\nJavaScript\n// Define our custom element with slot event handling\nclass DynamicList extends HTMLElement {\nconstructor() {\nsuper();\n// Create a shadow root\nthis.attachShadow({ mode: 'open' });\n// Create the component structure\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 20px;\nbackground-color: #fff;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n.list-container {\nmargin: 0;\npadding: 0;\n}\n.list-header {\ndisplay: flex;\njustify-content: space-between;\nalign-items: center;\nborder-bottom: 1px solid #eee;\npadding-bottom: 10px;\nmargin-bottom: 10px;\n}\n.count {\nbackground-color: #e0e0e0;\nborder-radius: 12px;\npadding: 2px 8px;\nfont-size: 14px;\n}\n::slotted(li) {\npadding: 8px 0;\nborder-bottom: 1px solid #f0f0f0;\nlist-style-position: inside;\n}\n::slotted(li:last-child) {\nborder-bottom: none;\n}\n</style>\n<div class=\"list-container\">\n<div class=\"list-header\">\n<h3>Dynamic List</h3>\n<span class=\"count\">2 items</span>\n</div>\n<ul>\n<slot></slot>\n</ul>\n</div>\n`;\n// Get references to elements\nthis.slotElement = this.shadowRoot.querySelector('slot');\nthis.countDisplay = this.shadowRoot.querySelector('.count');\n// Bind methods to this instance\nthis.updateCount = this.updateCount.bind(this);\n// Add slotchange event listener\nthis.slotElement.addEventListener('slotchange', this.updateCount);\n}\n// Called when the element is inserted into the DOM\nconnectedCallback() {\n// Update count on initial render\nthis.updateCount();\n}\n// Update the count when slot content changes\nupdateCount() {\n// Count the actual children of the custom element instead of using assignedElements\nconst count = this.childElementCount;\nthis.countDisplay.textContent = `${count} item${count !== 1 ? 's' : ''}`;\n// Log the current items for demonstration\nconsole.log('List items updated:', count);\n}\n// Clean up when element is removed\ndisconnectedCallback() {\nthis.slotElement.removeEventListener('slotchange', this.updateCount);\n}}\n// Register our custom element\ncustomElements.define('dynamic-list', DynamicList);\n// Control buttons functionality\ndocument.getElementById('add-item').addEventListener('click', () => {\nconst list = document.querySelector('dynamic-list');\nconst newItem = document.createElement('li');\nnewItem.textContent = `New item ${list.children.length + 1}`;\nlist.appendChild(newItem);\n// Force count update in case the slotchange event doesn't trigger\nlist.updateCount();\n});\ndocument.getElementById('remove-item').addEventListener('click', () => {\nconst list = document.querySelector('dynamic-list');\nif (list.lastElementChild) {\nlist.removeChild(list.lastElementChild);\n// Force count update in case the slotchange event doesn't trigger\nlist.updateCount();\n}\n});\ndocument.getElementById('clear-items').addEventListener('click', () => {\nconst list = document.querySelector('dynamic-list');\nwhile (list.firstChild) {\nlist.removeChild(list.firstChild);\n}\n// Force count update in case the slotchange event doesn't trigger\nlist.updateCount();\n});\nWorking with Slot Events on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nHTML\n<h1>Template Inside a Component Example</h1>\n<p>This example shows how to use templates within a web component to create dynamic content based on data.</p>\n<div class=\"controls\">\n<button id=\"add-task\">Add Random Task</button>\n<button id=\"complete-task\">Complete Random Task</button>\n<button id=\"clear-tasks\">Clear All Tasks</button>\n</div>\n<task-list></task-list>\nCSS\nbody {\nfont-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\nmax-width: 800px;\nmargin: 0 auto;\npadding: 20px;\n}\n.controls {\nmargin: 20px 0;\ndisplay: flex;\ngap: 10px;\nflex-wrap: wrap;\n}\nbutton {\npadding: 8px 16px;\nbackground-color: #4CAF50;\ncolor: white;\nborder: none;\nborder-radius: 4px;\ncursor: pointer;\n}\nJavaScript\n// Define our custom element with internal template\nclass TaskList extends HTMLElement {\nconstructor() {\nsuper();\n// Initialize tasks array\nthis.tasks = [\n{ id: 1, title: 'Learn about Web Components', completed: false },\n{ id: 2, title: 'Understand Shadow DOM', completed: false },\n{ id: 3, title: 'Master Templates and Slots', completed: false }];\n// Create a shadow root\nthis.attachShadow({ mode: 'open' });\n// Create the component structure with an internal template\nthis.shadowRoot.innerHTML = `\n<style>\n:host {\ndisplay: block;\nborder: 1px solid #ddd;\nborder-radius: 8px;\npadding: 20px;\nbackground-color: #fff;\nbox-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\nh3 {\nmargin-top: 0;\nborder-bottom: 1px solid #eee;\npadding-bottom: 10px;\n}\n.task-count {\ncolor: #666;\nfont-size: 14px;\nmargin-left: 10px;\nfont-weight: normal;\n}\n.empty-state {\npadding: 20px;\ntext-align: center;\ncolor: #888;\nfont-style: italic;\n}\n.task {\ndisplay: flex;\nalign-items: center;\npadding: 10px 0;\nborder-bottom: 1px solid #f0f0f0;\n}\n.task:last-child {\nborder-bottom: none;\n}\n.task.completed .task-title {\ntext-decoration: line-through;\ncolor: #888;\n}\n.checkbox {\nmargin-right: 10px;\nappearance: none;\nwidth: 18px;\nheight: 18px;\nborder: 2px solid #aaa;\nborder-radius: 4px;\noutline: none;\ncursor: pointer;\nposition: relative;\n}\n.checkbox:checked {\nbackground-color: #4CAF50;\nborder-color: #4CAF50;\n}\n.checkbox:checked::after {\ncontent: \"✓\";\nposition: absolute;\ncolor: white;\nfont-size: 14px;\ntop: -2px;\nleft: 3px;\n}\n.task-title {\nflex: 1;\n}\n.delete-btn {\nbackground: none;\nborder: none;\ncolor: #f44336;\ncursor: pointer;\nfont-size: 16px;\nopacity: 0.7;\n}\n.delete-btn:hover {\nopacity: 1;\n}\n</style>\n<div class=\"task-list-container\">\n<h3>Task List <span class=\"task-count\">(0 tasks)</span></h3>\n<div class=\"tasks-container\"></div>\n</div>\n<!-- Template for individual task items -->\n<template id=\"task-template\">\n<div class=\"task\">\n<input type=\"checkbox\" class=\"checkbox\">\n<span class=\"task-title\"></span>\n<button class=\"delete-btn\">✕</button>\n</div>\n</template>\n<!-- Template for empty state -->\n<template id=\"empty-template\">\n<div class=\"empty-state\">\nNo tasks found. Add a task to get started.\n</div>\n</template>\n`;\n// Bind methods\nthis.renderTasks = this.renderTasks.bind(this);\nthis.toggleTask = this.toggleTask.bind(this);\nthis.deleteTask = this.deleteTask.bind(this);\n// Render initial tasks\nthis.renderTasks();\n}\n// Connect component event listeners\nconnectedCallback() {\nthis.shadowRoot.addEventListener('click', e => {\nif (e.target.classList.contains('checkbox')) {\nconst taskId = parseInt(e.target.dataset.id);\nthis.toggleTask(taskId);\n} else if (e.target.classList.contains('delete-btn')) {\nconst taskId = parseInt(e.target.dataset.id);\nthis.deleteTask(taskId);\n}\n});\n}\n// Add a new task\naddTask(title) {\nconst newTask = {\nid: Date.now(), // Use timestamp as unique ID\ntitle: title,\ncompleted: false };\nthis.tasks.push(newTask);\nthis.renderTasks();\nreturn newTask;\n}\n// Toggle task completion status\ntoggleTask(taskId) {\nthis.tasks = this.tasks.map(task => {\nif (task.id === taskId) {\nreturn { ...task, completed: !task.completed };\n}\nreturn task;\n});\nthis.renderTasks();\n}\n// Delete a task\ndeleteTask(taskId) {\nthis.tasks = this.tasks.filter(task => task.id !== taskId);\nthis.renderTasks();\n}\n// Render all tasks using the template\nrenderTasks() {\nconst container = this.shadowRoot.querySelector('.tasks-container');\nconst taskTemplate = this.shadowRoot.getElementById('task-template');\nconst emptyTemplate = this.shadowRoot.getElementById('empty-template');\n// Update task count\nthis.shadowRoot.querySelector('.task-count').textContent =\n`(${this.tasks.length} task${this.tasks.length !== 1 ? 's' : ''})`;\n// Clear container\ncontainer.innerHTML = '';\n// Show empty state if no tasks\nif (this.tasks.length === 0) {\nconst emptyState = emptyTemplate.content.cloneNode(true);\ncontainer.appendChild(emptyState);\nreturn;\n}\n// Add each task using the template\nthis.tasks.forEach(task => {\nconst taskElement = taskTemplate.content.cloneNode(true);\nconst checkbox = taskElement.querySelector('.checkbox');\nconst title = taskElement.querySelector('.task-title');\nconst deleteBtn = taskElement.querySelector('.delete-btn');\n// Set task data\ncheckbox.checked = task.completed;\ncheckbox.dataset.id = task.id;\ntitle.textContent = task.title;\ndeleteBtn.dataset.id = task.id;\n// Add completed class if needed\nif (task.completed) {\ntaskElement.querySelector('.task').classList.add('completed');\n}\n// Add to container\ncontainer.appendChild(taskElement);\n});\n}}\n// Register our custom element\ncustomElements.define('task-list', TaskList);\n// Control buttons functionality\nconst taskList = document.querySelector('task-list');\nconst sampleTasks = [\n'Read Web Components documentation',\n'Build a custom component library',\n'Create a portfolio website',\n'Learn about ARIA attributes',\n'Study Shadow DOM encapsulation',\n'Practice with CSS custom properties',\n'Build an accessible component',\n'Refactor legacy code',\n'Write unit tests'];\ndocument.getElementById('add-task').addEventListener('click', () => {\n// Pick a random task from sample tasks\nconst randomTask = sampleTasks[Math.floor(Math.random() * sampleTasks.length)];\ntaskList.addTask(randomTask);\n});\ndocument.getElementById('complete-task').addEventListener('click', () => {\n// Get incomplete tasks\nconst incompleteTasks = taskList.tasks.filter(task => !task.completed);\nif (incompleteTasks.length === 0) return;\n// Pick a random incomplete task\nconst randomTask = incompleteTasks[Math.floor(Math.random() * incompleteTasks.length)];\ntaskList.toggleTask(randomTask.id);\n});\ndocument.getElementById('clear-tasks').addEventListener('click', () => {\ntaskList.tasks = [];\ntaskList.renderTasks();\n});\nTemplate Inside a Component on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/).\nThese examples showcase the versatility of templates and slots beyond basic usage. Slots Fallback Content creates resilient components with sensible defaults, improving developer experience while maintaining visual consistency. Slot Events transform components from passive containers into reactive elements that adapt to their content, though remember what slotchange does and does not report: it fires when the set of nodes assigned to a slot changes (and can fire once on first render if the slot starts with content). It does not fire when the text inside an already-slotted element changes. Templates Inside Components demonstrate composition at its best, enabling dynamic interfaces that render different structures based on data or user interaction—particularly valuable for complex interfaces that must adapt to changing requirements.\nConclusion\nYou can now write a component's layout once as a <template>, clone it into a shadow root, and leave labeled <slot> holes that each page fills with its own markup, with fallback text when it fills nothing.\nOne limit remains: the layout is fixed, and the page's CSS cannot restyle what lives inside the shadow tree. The next question is how a component should let outside styles in on purpose, which is styling.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The question and answer are preserved; scoring, retry, and remembered attempts are unavailable. Review: Review the static question and answer; keep the canonical link when scoring or retries matter.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nPortable fallback: The HTML, CSS, and JavaScript sources are preserved; the live preview remains on the canonical article. Review: Review the preserved source and keep the canonical link for editing or execution.\nJoin the discussion on the canonical article (https://mnixblog.netlify.app/how-templates-and-slots-work/#ob-discussion)",
      "date_modified": "2026-10-26T00:00:00Z",
      "date_published": "2025-03-09T05:00:51Z",
      "id": "https://mnixblog.netlify.app/how-templates-and-slots-work/",
      "image": "https://mnixblog.netlify.app/images/how-templates-and-slots-work/cover-v4.jpg",
      "summary": "Use template for a fixed layout and slot for the blanks a page fills in, then combine them in a web component.",
      "tags": [
        "JavaScript",
        "Web Components"
      ],
      "title": "How Templates and Slots Work",
      "url": "https://mnixblog.netlify.app/how-templates-and-slots-work/"
    }
  ],
  "language": "en",
  "title": "Journey to Code",
  "version": "https://jsonfeed.org/version/1.1"
}