@psu-flex/chrome-elements
v1.0.4
Published
Vanilla HTML / CSS / JS for PSU chrome (brand bar, mega menu header, site footer, brand footer).
Readme
@psu-flex/chrome-elements
Vanilla HTML / CSS / JS for PSU chrome (brand bar, mega menu header, site footer, brand footer).
No React, no Contentful client, no fetch. Render needs a props object — not a live API call. You can:
- Build that object yourself (static JSON, CMS, your own backend), or
- Pass a raw
fetchAllFederatedDatapayload and letmap-propsconvert it.
Same render functions power both internal Next.js (document CSS, no shadow DOM) and external custom elements (shadow DOM via @psu-flex/core-ui-federated-wc).
Package split
| Package | Role |
| ------- | ---- |
| @psu-flex/chrome-elements | Render, map-props, behavior, types, CSS. Optional custom-element wrappers. |
| @psu-flex/core-ui-federated-wc | Registers <psu-*> tags for Angular / Drupal / static HTML. |
| @psu-flex/wc-styles | Document fonts for external hosts (@font-face cannot live in shadow DOM). |
Internal Flex Next.js apps should use BrandBar.server / BrandBar.ctf (and Footer / BrandFooter / MegaMenuHeader equivalents) in @psu-flex/core-ui-federated and @psu-flex/core-ui-layout-navigation, not the custom elements. *New.server / *New.ctf still work as aliases.
Hydrate without the API
Each component has a mapped props type. If you assign that shape, map-props returns it unchanged and render* paints the chrome.
How the mapper decides:
- Payload has
__typenameorsys→ treated as raw Contentful /fetchAllFederatedData. - Otherwise, if the mapped fields are present (see below) → used as-is.
Do not put __typename or sys on a hand-built object, or it will be treated as raw CMS data.
Next.js / React — custom data, no API
Flex Next.js apps should not use <psu-*> custom elements. Pass mapped props into the .server wrappers (BrandBar.server, MegaMenuHeader.server, Footer.server, BrandFooter.server). They SSR the HTML, attach behavior on the client, and import component CSS. Theme tokens still load on the document.
*New.server paths still work; they are aliases of the names below.
app/layout.tsx:
import '@psu-flex/platform-theme/global.css';
import '@psu-flex/platform-theme/theme-variables.css';
import '@psu-flex/platform-theme/theme-utilities.css';
import { BrandBarServer } from '@psu-flex/core-ui-federated/BrandBar.server';
import { BrandFooterServer } from '@psu-flex/core-ui-federated/BrandFooter.server';
import { MegaMenuHeaderServer } from '@psu-flex/core-ui-layout-navigation/MegaMenuHeader.server';
import { FooterServer } from '@psu-flex/core-ui-layout-navigation/Footer.server';
import type {
BrandBarProps,
MegaMenuHeaderProps,
FooterProps,
BrandFooterProps,
} from '@psu-flex/chrome-elements';
const brandBar: BrandBarProps = {
isExternal: false,
searchAction: '/search',
brandBarHeading: 'Search this site',
ctaItems: [{ label: 'Apply', url: '/admission' }],
flyoutHelpfulLinksHeading: 'Helpful links',
flyoutHelpfulLinksItems: [{ label: 'Academic calendar', url: '/calendar' }],
flyoutInformationLinksHeading: 'Information for',
flyoutInformationLinksItems: [{ label: 'Current students', url: '/students' }],
};
const header: MegaMenuHeaderProps = {
tierTwoEntity: { label: 'College of Earth and Mineral Sciences', url: '/' },
ctaButtonsCollection: [{ label: 'Apply', url: '/admission' }],
navigationCollection: [
{
label: 'Academics',
url: '/academics',
referenceCollection: {
items: [
{
linkContentCollection: {
items: [
{ type: 'Heading Link', label: 'Undergraduate', url: '/academics/undergraduate' },
],
},
},
],
},
},
],
};
const footer: FooterProps = {
institutionName: 'Penn State',
departmentName: 'Department of Geosciences',
streetAddress: '503 Deike Building',
city: 'University Park',
state: 'PA',
zipCode: '16802',
contactItems: [{ label: 'Phone', href: 'tel:8148656711', value: '814-865-6711' }],
navItems: [
{
label: 'Explore',
linkItemsCollection: { items: [{ title: 'About', to: '/about' }] },
},
],
};
const brandFooter: BrandFooterProps = {
linkItemsCollection: [{ label: 'Privacy', url: 'https://www.psu.edu/privacy' }],
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<BrandBarServer props={brandBar} />
<MegaMenuHeaderServer props={header} />
<main>
<a id="main-content" tabIndex={-1} />
{children}
</main>
<FooterServer props={footer} />
<BrandFooterServer props={brandFooter} />
</body>
</html>
);
}That is the same markup Flex apps use with Contentful (BrandBar.ctf, etc.), except you skip *.ctf and pass your own objects. Field lists: Mapped props contracts.
If you have raw federated JSON instead of mapped props, map once then pass the result:
import { mapBrandBarProps } from '@psu-flex/chrome-elements/BrandBar.map-props';
<BrandBarServer props={mapBrandBarProps(rawBrandBar)} />1. Assign props on a custom element
Works after definePsuBrandBar() (or defineAllPsuElements() from federated-wc):
import { definePsuBrandBar } from '@psu-flex/chrome-elements/BrandBar';
import type { BrandBarProps } from '@psu-flex/chrome-elements';
definePsuBrandBar();
const el = document.querySelector('psu-brand-bar') as HTMLElement & {
props: BrandBarProps;
};
el.props = {
isExternal: true,
brandBarHeading: 'Search Penn State',
ctaItems: [{ label: 'Apply', url: 'https://www.psu.edu/admission' }],
flyoutHelpfulLinksHeading: 'Helpful links',
flyoutHelpfulLinksItems: [
{ label: 'Academic calendar', url: 'https://www.psu.edu/academic-calendar' },
],
flyoutInformationLinksHeading: 'Information for',
flyoutInformationLinksItems: [
{ label: 'Current students', url: 'https://www.psu.edu/current-students' },
],
extraSearchOptions: [
{
label: 'All Penn State',
value: 'All Penn State',
slug: 'https://www.psu.edu/search',
},
],
};The same pattern applies to header, footer, and brand footer (el.props = mapped).
2. Call render + behavior directly (no custom element)
Use this in a host that already owns a container (Next.js .server wrappers, tests, static HTML):
import { renderBrandBar } from '@psu-flex/chrome-elements/BrandBar.render';
import { initBrandBar } from '@psu-flex/chrome-elements/BrandBar.behavior';
import type { BrandBarProps } from '@psu-flex/chrome-elements';
const props: BrandBarProps = {
isExternal: true,
ctaItems: [{ label: 'Apply', url: 'https://www.psu.edu/admission' }],
};
const root = document.querySelector('#chrome-brand-bar') as HTMLElement;
renderBrandBar(root, props);
initBrandBar(root);Load component CSS on the document for this path:
import '@psu-flex/chrome-elements/BrandBar.css';
// plus theme tokens in the host app:
import '@psu-flex/platform-theme/theme-variables.css';3. data attribute / hydrateChromeElementData
Serialized JSON of either mapped props or a raw federated payload:
import { hydrateChromeElementData } from '@psu-flex/chrome-elements/hydrate';
hydrateChromeElementData(document.querySelector('psu-brand-bar'), mappedBrandBarProps);Mapped props contracts
Import types from @psu-flex/chrome-elements. Collections on mapped objects are arrays, not Contentful { items: [] } wrappers — except where the type still nests *.items (header columns, footer nav groups).
Brand bar — BrandBarProps
Detected as mapped when ctaItems is an array.
| Field | Type | Notes |
| ----- | ---- | ----- |
| ctaItems | { label, url }[] | Top-bar links |
| brandBarHeading | string | Flyout heading |
| flyoutHelpfulLinksHeading / flyoutHelpfulLinksItems | string / { label, url }[] | |
| flyoutInformationLinksHeading / flyoutInformationLinksItems | string / { label, url }[] | |
| isExternal | boolean | Custom elements default to true (search → psu.edu). Internal Next sets false. |
| searchAction | string | Override form action. Omit on external hosts. |
| currentSiteSearchOption | boolean | Show “Current Site” radio (default true) |
| extraSearchOptions | { label, value, slug?, queryParamName?, urlToInclude? }[] | Extra radios |
Mega menu header — MegaMenuHeaderProps
Detected as mapped when navigationCollection is an array.
import type { MegaMenuHeaderProps } from '@psu-flex/chrome-elements';
const header: MegaMenuHeaderProps = {
tierOneMark: { url: '/wordmark.svg' },
tierTwoEntity: { label: 'College of Earth and Mineral Sciences', url: '/' },
tierThreeDepartment: { label: 'Geosciences', url: '/geosciences' },
ctaButtonsCollection: [{ label: 'Apply', url: 'https://www.psu.edu/admission' }],
navigationCollection: [
{
label: 'Academics',
url: '/academics',
referenceCollection: {
items: [
{
linkContentCollection: {
items: [
{ type: 'Heading Link', label: 'Undergraduate', url: '/academics/undergraduate' },
{ type: 'External URL', label: 'Bulletin', url: 'https://bulletins.psu.edu' },
],
},
},
],
},
},
],
};Site footer — FooterProps
Detected as mapped when navItems or contactItems is an array.
import type { FooterProps } from '@psu-flex/chrome-elements';
const footer: FooterProps = {
tierOneMark: { url: '/wordmark.svg' },
tierTwoEntity: { title: 'College of Earth and Mineral Sciences', href: '/' },
departmentName: 'Department of Geosciences',
institutionName: 'Penn State',
streetAddress: '503 Deike Building',
city: 'University Park',
state: 'PA',
zipCode: '16802',
contactItems: [
{ label: 'Phone', href: 'tel:8148656711', value: '814-865-6711' },
{ label: 'Email', href: 'mailto:[email protected]', value: '[email protected]' },
],
socialIcons: [{ label: 'Facebook', href: 'https://facebook.com/pennstate' }],
navItems: [
{
label: 'Explore',
linkItemsCollection: {
items: [
{ title: 'About', to: '/about' },
{ title: 'News', to: '/news' },
],
},
},
],
};Brand footer — BrandFooterProps
Detected as mapped when linkItemsCollection is an array.
import type { BrandFooterProps } from '@psu-flex/chrome-elements';
const brandFooter: BrandFooterProps = {
linkItemsCollection: [
{ label: 'Privacy', url: 'https://www.psu.edu/privacy' },
{ label: 'Non-discrimination', url: 'https://www.psu.edu/non-discrimination' },
],
};Optional: map a federated API payload
If you already have fetchAllFederatedData JSON, pass it through the mapper (or assign it as props / data and let the custom element map it):
import { mapBrandBarProps } from '@psu-flex/chrome-elements/BrandBar.map-props';
import { mapMegaMenuHeaderProps } from '@psu-flex/chrome-elements/MegaMenuHeader.map-props';
import { mapFooterProps } from '@psu-flex/chrome-elements/Footer.map-props';
import { mapBrandFooterProps } from '@psu-flex/chrome-elements/BrandFooter.map-props';
const brandBar = mapBrandBarProps(apiJson.brandBar);
const header = mapMegaMenuHeaderProps(apiJson.header);
const footer = mapFooterProps(apiJson.footer);
const brandFooter = mapBrandFooterProps(apiJson.brandFooter);Endpoints (optional convenience, not required):
Exports (per component)
Canonical names (no New). The *New subpaths still resolve to the same files.
| Subpath | Purpose |
| ------- | ------- |
| ./BrandBar.render (and Header / Footer / BrandFooter) | render*(el, mappedProps) |
| ./BrandBar.map-props | Raw CMS/API → mapped props (mapBrandBarProps) |
| ./BrandBar.behavior | Click / keyboard / search after render |
| ./BrandBar.element | Shadow-DOM custom element class |
| ./BrandBar | definePsuBrandBar() registrar |
| ./BrandBar.css | Document CSS (Next.js / non-shadow) |
| ./BrandBarTypes | Prop types (BrandBarProps) |
| ./hydrate | hydrateChromeElementData |
| ./fonts | injectPsuFonts() |
| ./document.css | All four component stylesheets |
@psu-flex/core-ui-federated/BrandBar and /BrandFooter are still the legacy Theme UI components. Use BrandBar.server / BrandBar.ctf (and the Footer / BrandFooter equivalents) for this chrome.
Related
@psu-flex/core-ui-federated-wc— register<psu-*>tags and hydrate all four at once@psu-flex/wc-styles— document fonts for external hosts
