diff --git a/packages/core/src/utilities/portal/index.ts b/packages/core/src/utilities/portal/index.ts
new file mode 100644
index 0000000..140833d
--- /dev/null
+++ b/packages/core/src/utilities/portal/index.ts
@@ -0,0 +1,2 @@
+export { Portal } from "./portal";
+export type { PortalProps } from "./portal";
diff --git a/packages/core/src/utilities/portal/portal.tsx b/packages/core/src/utilities/portal/portal.tsx
new file mode 100644
index 0000000..c5ce33e
--- /dev/null
+++ b/packages/core/src/utilities/portal/portal.tsx
@@ -0,0 +1,25 @@
+import type { JSX } from "solid-js";
+import { isServer } from "solid-js/web";
+import { Portal as SolidPortal } from "solid-js/web";
+
+export interface PortalProps {
+ /** Target container. Defaults to document.body. */
+ target?: Element | null;
+ children: JSX.Element;
+}
+
+/**
+ * SSR-safe portal. During SSR, renders content inline.
+ * On the client, moves content to the target container.
+ */
+export function Portal(props: PortalProps): JSX.Element {
+ if (isServer) {
+ return <>{props.children}>;
+ }
+
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/packages/core/src/utilities/visually-hidden/index.ts b/packages/core/src/utilities/visually-hidden/index.ts
new file mode 100644
index 0000000..763840b
--- /dev/null
+++ b/packages/core/src/utilities/visually-hidden/index.ts
@@ -0,0 +1,2 @@
+export { VisuallyHidden } from "./visually-hidden";
+export type { VisuallyHiddenProps } from "./visually-hidden";
diff --git a/packages/core/src/utilities/visually-hidden/visually-hidden.tsx b/packages/core/src/utilities/visually-hidden/visually-hidden.tsx
new file mode 100644
index 0000000..51cf641
--- /dev/null
+++ b/packages/core/src/utilities/visually-hidden/visually-hidden.tsx
@@ -0,0 +1,34 @@
+import type { JSX } from "solid-js";
+import { splitProps } from "solid-js";
+
+export interface VisuallyHiddenProps extends JSX.HTMLAttributes {
+ children?: JSX.Element;
+}
+
+const visuallyHiddenStyle: JSX.CSSProperties = {
+ position: "absolute",
+ border: "0",
+ width: "1px",
+ height: "1px",
+ padding: "0",
+ margin: "-1px",
+ overflow: "hidden",
+ clip: "rect(0, 0, 0, 0)",
+ "white-space": "nowrap",
+ "word-wrap": "normal",
+};
+
+/**
+ * Renders content visually hidden but accessible to screen readers.
+ */
+export function VisuallyHidden(props: VisuallyHiddenProps): JSX.Element {
+ const [local, rest] = splitProps(props, ["children", "style"]);
+ return (
+
+ {local.children}
+
+ );
+}
diff --git a/packages/core/tests/utilities/portal.test.tsx b/packages/core/tests/utilities/portal.test.tsx
new file mode 100644
index 0000000..3f6dc7a
--- /dev/null
+++ b/packages/core/tests/utilities/portal.test.tsx
@@ -0,0 +1,23 @@
+import { render } from "@solidjs/testing-library";
+import { describe, expect, it } from "vitest";
+import { Portal } from "../../src/utilities/portal/portal";
+
+describe("Portal", () => {
+ it("renders children into document.body by default", () => {
+ render(() => hello
);
+ // Content should be in document.body, not the render container
+ expect(document.body.querySelector("[data-testid='portal-content']")).toBeTruthy();
+ });
+
+ it("renders children into a custom target", () => {
+ const target = document.createElement("div");
+ document.body.appendChild(target);
+ render(() => (
+
+ hello
+
+ ));
+ expect(target.querySelector("[data-testid='custom-portal']")).toBeTruthy();
+ document.body.removeChild(target);
+ });
+});