diff --git a/src/AcDream.App/UI/Layout/UiDatElement.cs b/src/AcDream.App/UI/Layout/UiDatElement.cs
new file mode 100644
index 00000000..892f053a
--- /dev/null
+++ b/src/AcDream.App/UI/Layout/UiDatElement.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Numerics;
+
+namespace AcDream.App.UI.Layout;
+
+///
+/// Generic dat element: draws its active state's media by DrawMode (Normal=tile,
+/// Alphablend/Overlay=blended overlay). The fallback renderer for every element type
+/// without a dedicated behavioral widget (chrome corners/edges, drag bars, resize grips);
+/// faithful because retail's base element render is exactly "stamp the media per draw-mode".
+///
+///
+/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the
+/// sprite shader already alpha-blends, so no per-mode branch is needed here. The named
+/// constants document the real enum for Plan 2.
+///
+///
+///
+/// DrawModeType (DatReaderWriter.Enums), stored as int in to
+/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6:
+/// Undefined=0, Normal=1, Overlay=2, Alphablend=3. There is no Stretch mode.
+///
+///
+///
+/// Tiling uses UV-repeat on BOTH axes (Width/tw, Height/th) so vertical
+/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too.
+/// sets
+/// GL_REPEAT on both S and T, so vertical tiling is always active.
+///
+///
+public sealed class UiDatElement : UiElement
+{
+ // DrawModeType enum values from DatReaderWriter.Enums.
+ // See docs/research/2026-06-15-layoutdesc-format.md §6.
+#pragma warning disable IDE0051 // private constants kept for documentation / Plan 2
+ private const int DrawUndefined = 0;
+ private const int DrawNormal = 1;
+ private const int DrawOverlay = 2;
+ private const int DrawAlphablend = 3;
+#pragma warning restore IDE0051
+
+ private readonly ElementInfo _info;
+ private readonly Func _resolve;
+
+ /// Which state name to render. "" = the unnamed DirectState.
+ /// Falls back to DirectState if the named state is absent.
+ public string ActiveState { get; set; } = "";
+
+ /// Merged for this element.
+ /// Dat file-id → (GL texture handle, native px width, native px height).
+ /// Returns (0,0,0) when the texture is not yet uploaded.
+ public UiDatElement(ElementInfo info, Func resolve)
+ {
+ _info = info;
+ _resolve = resolve;
+ ClickThrough = true; // generic decoration; behavioral widgets opt back in
+ }
+
+ ///
+ /// Returns the (File, DrawMode) for the current ,
+ /// falling back to the DirectState ("" key) if the named state is absent.
+ /// Returns (0, 0) if neither exists.
+ ///
+ public (uint File, int DrawMode) ActiveMedia()
+ => _info.StateMedia.TryGetValue(ActiveState, out var m) ? m
+ : _info.StateMedia.TryGetValue("", out var d) ? d
+ : (0u, 0);
+
+ protected override void OnDraw(UiRenderContext ctx)
+ {
+ var (file, drawMode) = ActiveMedia();
+ if (file == 0) return;
+
+ var (tex, tw, th) = _resolve(file);
+ if (tex == 0 || tw == 0 || th == 0) return;
+
+ // Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI texture),
+ // matching ImgTex::TileCSI. Overlay/Alphablend are the same blit with a blend state; the
+ // sprite shader already alpha-blends, so the quad is identical for all draw modes in Plan 1.
+ // (No Stretch mode exists in DatReaderWriter.Enums.DrawModeType.)
+ // drawMode is not yet branched here — Plan 2 can add per-mode behavior if needed.
+ _ = drawMode; // suppress unused-variable warning until Plan 2 adds per-mode branches
+ float u1 = Width / tw;
+ float v1 = Height / th;
+ ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, u1, v1, Vector4.One);
+ }
+}
diff --git a/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs
new file mode 100644
index 00000000..91f66d49
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/Layout/UiDatElementTests.cs
@@ -0,0 +1,17 @@
+using AcDream.App.UI.Layout;
+namespace AcDream.App.Tests.UI.Layout;
+
+public class UiDatElementTests
+{
+ [Fact]
+ public void ActiveMedia_PrefersNamedStateOverDirect()
+ {
+ var info = new ElementInfo();
+ info.StateMedia[""] = (0x06000001, 1); // DirectState (DrawMode Normal=1)
+ info.StateMedia["ShowDetail"] = (0x06000002, 3); // named (Alphablend=3)
+ var e = new UiDatElement(info, _ => (0, 0, 0)) { ActiveState = "ShowDetail" };
+ Assert.Equal(0x06000002u, e.ActiveMedia().File);
+ e.ActiveState = "";
+ Assert.Equal(0x06000001u, e.ActiveMedia().File);
+ }
+}