Jumping Into Webmentions With NextJS (or Not)

Webmention is a W3C recommendation last published on January 12, 2017. And what exactly is a Webmention? It’s described as…

[…] a simple way to notify any URL when you mention it on your site. From the receiver’s perspective, it’s a way to request notifications when other sites mention it.

In a nutshell, it’s a way of letting a website know it has been mentioned somewhere, by someone, in some way. The Webmention spec also describes it as a way for a website to let others know it cited them. What that basically bails down to is that your website is an active social media channel, channeling communication from other channels (e.g. Twitter, Instagram, Mastodon, Facebook, etc.).

How does a site implement Webmentions? In some cases, like WordPress, it’s as trivial as installing a couple of plugins. Other cases may not be quite so simple, but it’s still pretty straightforward. In fact, let’s do that now!

Here’s our plan

  1. Declare an endpoint to receive Webmentions
  2. Process social media interactions to Webmentions
  3. Get those mentions into a website/app
  4. Set the outbound Webmentions

Luckily for us, there are services in place that make things extremely simple. Well, except that third point, but hey, it’s not so bad and I’ll walk through how I did it on my own atila.io site.

My site is a server-side blog that’s pre-rendered and written with NextJS. I have opted to make Webmention requests client-side; therefore, it will work easily in any other React app and with very little refactoring in any other JavaScript application.

Step 1: Declare an endpoint to receive Webmentions

In order to have an endpoint we can use to accept Webmentions, we need to either write the script and add to our own server, or use a service such as Webmention.io (which is what I did).

Webmention.io is free and you only need to confirm ownership over the domain you register. Verification can happen a number of ways. I did it by adding a rel="me" attribute to a link in my website to my social media profiles. It only takes one such link, but I went ahead and did it for all of my accounts.

<a   href="https://twitter.com/atilafassina"   target="_blank"   rel="me noopener noreferrer" >   @AtilaFassina </a>

Verifying this way, we also need to make sure there’s a link pointing back to our website in that Twitter profile. Once we’ve done that, we can head back to Webmention.io and add the URL.

This gives us an endpoint for accepting Webmentions! All we need to do now is wire it up as <link> tags in the <head> of our webpages in order to collect those mentions.

<head>   <link rel="webmention" href="https://webmention.io/{user}/webmention" />   <link rel="pingback" href="https://webmention.io/{user}/xmlrpc" />   <!-- ... --> </head>

Remember to replace {user} with your Webmention.io username.

Step 2: Process social media interactions into Webmentions

We are ready for the Webmentions to start flowing! But wait, we have a slight problem: nobody actually uses them. I mean, I do, you do, Max Böck does, Swyx does, and… that’s about it. So, now we need to start converting all those juicy social media interactions into Webmentions.

And guess what? There’s an awesome free service for it. Fair warning though: you’d better start loving the IndieWeb because we’re about to get all up in it.

Bridgy connects all our syndicated content and converts them into proper Webmentions so we can consume it. With a SSO, we can get each of our profiles lined up, one by one.

Step 3: Get those mentions into a website/app

Now it’s our turn to do some heavy lifting. Sure, third-party services can handle all our data, but it’s still up to us to use it and display it.

We’re going to break this up into a few stages. First, we’ll get the number of Webmentions. From there, we’ll fetch the mentions themselves. Then we’ll hook that data up to NextJS (but you don’t have to), and display it.

Get the number of mentions

type TMentionsCountResponse = {   count: number   type: {     like: number     mention: number     reply: number     repost: number   } }

That is an example of an object we get back from the Webmention.io endpoint. I formatted the response a bit to better suit our needs. I’ll walk through how I did that in just a bit, but here’s the object we will get:

type TMentionsCount = {   mentions: number   likes: number   total: number }

The endpoint is located at:

https://webmention.io/api/count.json?target=$  {post_url}

The request will not fail without it, but the data won’t come either. Both Max Böck and Swyx combine likes with reposts and mentions with replies. In Twitter, they are analogous.

const getMentionsCount = async (postURL: string): TMentionsCount => {   const resp = await fetch(     `https://webmention.io/api/count.json?target=$  {postURL}/`   )   const { type, count } = await resp.json() 
   return {     likes: type.like + type.repost,     mentions: type.mention + type.reply,     total: count,   } }

Get the actual mentions

Before getting to the response, please note that the response is paginated, where the endpoint accepts three parameters in the query:

  • page: the page being requested
  • per-page: the number of mentions to display on the page
  • target: the URL where Webmentions are being fetched

Once we hit https://webmention.io/api/mentions and pass the these params, the successful response will be an object with a single key links which is an array of mentions matching the type below:

type TMention = {   source: string   verified: boolean   verified_date: string // date string   id: number   private: boolean   data: {     author: {       name: string       url: string       photo: string // url, hosted in webmention.io     }     url: string     name: string     content: string // encoded HTML     published: string // date string     published_ts: number // ms   }   activity: {     type: 'link' | 'reply' | 'repost' | 'like'     sentence: string // pure text, shortened     sentence_html: string // encoded html   }   target: string }

The above data is more than enough to show a comment-like section list on our site. Here’s how the fetch request looks in TypeScript:

const getMentions = async (   page: string,   postsPerPage: number,   postURL: string ): { links: TWebMention[] } => {   const resp = await fetch(     `https://webmention.io/api/mentions?page=$  {page}&per-page=$  {postsPerPage}&target=$  {postURL}`   )   const list = await resp.json()   return list.links }

Hook it all up in NextJS

We’re going to work in NextJS for a moment. It’s all good if you aren’t using NextJS or even have a web app. We already have all the data, so those of you not working in NextJS can simply move ahead to Step 4. The rest of us will meet you there.

As of version 9.3.0, NextJS has three different methods for fetching data:

  1. getStaticProps: fetches data on build time
  2. getStaticPaths: specifies dynamic routes to pre-render based on the fetched data
  3. getServerSideProps: fetches data on each request

Now is the moment to decide at which point we will be making the first request for fetching mentions. We can pre-render the data on the server with the first batch of mentions, or we can make the entire thing client-side. I opted to go client-side.

If you’re going client-side as well, I recommend using SWR. It’s a custom hook built by the Vercel team that provides good caching, error and loading states — it and even supports React.Suspense.

Display the Webmention count

Many blogs show the number of comments on a post in two places: at the top of a blog post (like this one) and at the bottom, right above a list of comments. Let’s follow that same pattern for Webmentions.

First off, let’s create a component for the count:

const MentionsCounter = ({ postUrl }) => {   const { t } = useTranslation()   // Setting a default value for `data` because I don't want a loading state   // otherwise you could set: if(!data) return <div>loading...</div>   const { data = {}, error } = useSWR(postUrl, getMentionsCount) 
   if (error) {     return <ErrorMessage>{t('common:errorWebmentions')}</ErrorMessage>   } 
   // The default values cover the loading state   const { likes = '-', mentions = '-' } = data 
   return (     <MentionCounter>       <li>         <Heart title="Likes" />         <CounterData>{Number.isNaN(likes) ? 0 : likes}</CounterData>       </li>       <li>         <Comment title="Mentions" />{' '}         <CounterData>{Number.isNaN(mentions) ? 0 : mentions}</CounterData>       </li>     </MentionCounter>   ) }

Thanks to SWR, even though we are using two instances of the WebmentionsCounter component, only one request is made and they both profit from the same cache.

Feel free to peek at my source code to see what’s happening:

Display the mentions

Now that we have placed the component, it’s time to get all that social juice flowing!

At of the time of this writing, useSWRpages is not documented. Add to that the fact that the webmention.io endpoint doesn’t offer collection information on a response (i.e. no offset or total number of pages), I couldn’t find a way to use SWR here.

So, my current implementation uses a state to keep the current page stored, another state to handle the mentions array, and useEffect to handle the request. The “Load More” button is disabled once the last request brings back an empty array.

const Webmentions = ({ postUrl }) => {   const { t } = useTranslation()   const [page, setPage] = useState(0)   const [mentions, addMentions] = useState([]) 
   useEffect(() => {     const fetchMentions = async () => {       const olderMentions = await getMentions(page, 50, postUrl)       addMentions((mentions) => [...mentions, ...olderMentions])     }     fetchMentions()   }, [page]) 
   return (     <>       {mentions.map((mention, index) => (         <Mention key={mention.data.author.name + index}>           <AuthorAvatar src={mention.data.author.photo} lazy />           <MentionContent>             <MentionText               data={mention.data}               activity={mention.activity.type}             />           </MentionContent>         </Mention>       ))}       </MentionList>       {mentions.length > 0 && (         <MoreButton           type="button"           onClick={() => {           setPage(page + 1)         }}         >         {t('common:more')}       </MoreButton>     )}     </>   ) }

The code is simplified to allow focus on the subject of this article. Again, feel free to peek at the full implementation:

Step 4: Handling outbound mentions

Thanks to Remy Sharp, handling outbound mentions from one website to others is quite easy and provides an option for each use case or preference possible.

The quickest and easiest way is to head over to Webmention.app, get an API token, and set up a web hook. Now, if you have RSS feed in place, the same thing is just as easy with an IFTT applet, or even a deploy hook.

If you prefer to avoid using yet another third-party service for this feature (which I totally get), Remy has open-sourced a CLI package called wm which can be ran as a postbuild script.

But that’s not enough to handle outbound mentions. In order for our mentions to include more than simply the originating URL, we need to add microformats to our information. Microformats are key because it’s a standardized way for sites to distribute content in a way that Webmention-enabled sites can consume.

At their most basic, microformats are a kind of class-based notations in markup that provide extra semantic meaning to each piece. In the case of a blog post, we will use two kinds of microformats:

  • h-entry: the post entry
  • h-card: the author of the post

Most of the required information for h-entry is usually in the header of the page, so the header component may end up looking something like this:

<header class="h-entry">   <!-- the post date and time -->   <time datetime="2020-04-22T00:00:00.000Z" class="dt-published">     2020-04-22   </time>   <!-- the post title -->   <h1 class="p-name">     Webmentions with NextJS   </h1> </header>

And that’s it. If you’re writing in JSX, remember to replace class with className, that datetime is camelCase (dateTime), and that you can use the new Date('2020-04-22').toISOString() function.

It’s pretty similar for h-card. In most cases (like mine), author information is below the article. Here’s how my page’s footer looks:

<footer class="h-card">   <!-- the author name -->   <span class="p-author">Atila Fassina</span>   <!-- the authot image-->   <img     alt="Author’s photograph: Atila Fassina"     class="u-photo"     src="/images/internal-avatar.jpg"     lazy   /> </footer>

Now, whenever we send an outbound mention from this blog post, it will display the full information to whomever is receiving it.

Wrapping up

I hope this post has helped you getting to know more about Webmentions (or even about IndieWeb as a whole), and perhaps even helped you add this feature to your own website or app. If it did, please consider sharing this post to your network. I will be super grateful! 😉


Further reading

The post Jumping Into Webmentions With NextJS (or Not) appeared first on CSS-Tricks.


, , ,

Design Deals for the Week


The principles of interaction design that you must know as a UI/UX designer


Chrome 83 Form Element Styles

There have been some aesthetic changes to what form elements look like as of Chrome 83. Anything with gradient colorization is gone (notably the extra-shiny <meter> stuff). The consistency across the board is nice, particularly between inputs and textareas. Not a big fan of the new <select> styling, but I hear a lot of accessibility research went into this, so it’s hard to complain there — plus you can always change it.

Hakim has a nice comparison tweet:

The Jetpack plugin for WordPress has a new comparison block and I’m going to try it out here. You can swipe between the items, just for fun (drag the slider in the middle):

This is not accompanied by new standardized ways to change the look of form elements with CSS, although browsers are well aware of that and seem to draw nearer and nearer all the time. I believe is was a step along that path.

I also see there is a new <input type="time"> as well. The old version looked like this and offered no UI controls:

Now we get this beast with controls:

There are no visual indicators or buttons, but you can scroll those columns.

Reddit notes that it uses the same pseudo element that date pickers use, so if you want it gone, you can scope it to these types of inputs (or not) and remove it.

input[type="time"]::-webkit-calendar-picker-indicator {   display: none; }

I’d call it an improvement (I like UI controls for things), but it does continue to highlight the need to be able to style these things, particularly if the goal is to have people actually use them and not (poorly) rebuild them.

The post Chrome 83 Form Element Styles appeared first on CSS-Tricks.


, , ,

Behind the Source: Cassie Evans

I feel like the tech industry takes itself far too seriously sometimes. I get frustrated by all the posturing and gatekeeping – “You’re not a real developer unless you use x framework”, “CSS isn’t a real programming language”.

I think this kind of rhetoric often puts new developers off, and the ones that don’t get put off are more inclined to skip over learning things like semantic markup and accessibility in favour of learning the latest framework.

Having a deeper knowledge of HTML and CSS is often devalued.

Posturing and gatekeeping, indeed. I’ve yet to witness a conversation where discussing what is or isn’t “real” programming was fruitful for anybody.

Related sentiment from Mehdi Zed about PHP:

Most developers who hate PHP hate it out of elitism or ignorance. Either way it’s dumb.

Direct Link to ArticlePermalink

The post Behind the Source: Cassie Evans appeared first on CSS-Tricks.


, , ,

Jetpack Scan

Fresh from the Jetpack team at Automattic, today, comes Jetpack Scan. Jetpack Scan scans all the files on your site looking for anything suspicious or malicious and lets you know, or literally fixes it for you with your one-click approval.

This kind of security scanning is very important to me. It’s one of those sleep better at night features, where I know I’m doing all I can do for the safety of my site.

It’s not fun to admit, but I bet in my decade-and-a-half of building WordPress sites, I’ve had half a dozen of them probably have some kind of malicious thing happen. It’s been a long time because I know more, take security way more seriously, and use proper tooling like this to make sure it can’t. But an example is that a malicious actor somehow edits files on your site. One edit to your wp-config.php file could easily take down your site. One edit to your single.php file could put malicious/spammy content on every single blog post. One sketchy plugin can literally do anything to your site. I want to know when any foul play is detected like this.

The new Jetpack.com Dashboard

I’m comforted by the idea that it is Automattic themselves who are checking my site every day and making sure it is clean. Aside from the fact that this is a paid service so they have all that incentive to make sure this does its job, they have the reputation of WordPress itself to uphold here, which is the kind of alignment I like to see in products.

If you’re a user or are familiar with VaultPress, which did backups and security scans, this is an evolution of that. This brings that world into a new dashboard on Jetpack.com (for scans and backup), meaning you can manage all this right from there. Note that this dashboard is for new customers of Jetpack Scan and Backup right now and will soon be available for all existing customers also.

That’s what Jetpack, more broadly, does: it brings powerful abilities of the WordPress.com cloud to your site. For example, backups, CDN hosted assets, instant search, related posts, automatic plugin updates, and more. All of that burden is lifted from your site and done on theirs.

Our page going into the many features of Jetpack we use on this site.

This is also another step toward more à la carte offerings from Jetpack. If you only want this feature and not anything else Jetpack offers, well, you’re in luck. Just like backups, that’s how this feature is sold. Want it? Pay just for it. Don’t want it? Don’t pay for it.

The intro offer (limited time) is $ 7/month or $ 70/year. So getting Jetpack Scan right away is your best value.

Their announcement post is out too. High five gang, very nice release.

The post Jetpack Scan appeared first on CSS-Tricks.



A New Way to Delay Keyframes Animations

If you’ve ever wanted to add a pause between each iteration of your CSS @keyframes animation, you’ve probably been frustrated to find there’s no built-in way to do it in CSS. Sure, we can delay the start of a set of @keyframes with animation-delay, but there’s no way to add time between the first iteration through the keyframes and each subsequent run. 

This came up when I wanted to adapt this shooting stars animation for use as the background of the homepage banner in a space-themed employee portal. I wanted to use fewer stars to reduce distraction from the main content, keep CPUs from melting, and still have the shooting stars seem random.

No pausing

For comparisons sake.

The “original” delay method

Here’s an example of where I applied the traditional keyframes delay technique to my fork of the shooting stars.

This approach involves figuring out how long we want the delay between iterations to be, and then compressing the keyframes to a fraction of 100%. Then, we maintain the final state of the animation until it reaches 100% to achieve the pause.

@keyframes my-animation {   /* Animation happens between 0% and 50% */   0% {     width: 0;   }   15% {     width: 100px;   }   /* Animation is paused/delayed between 50% and 100% */   50%, 100% {     width: 0;   } }

I experienced the main drawback of this approach: each keyframe has to be manually tweaked, which is mildly painful and certainly prone to error. It’s also harder to understand what the animation is doing if it requires mentally transposing all the keyframes back up to 100%.

New technique: hide during the delay

Another technique is to create a new set of @keyframes that is responsible for hiding the animation during the delay. Then, apply that with the original animation, at the same time.

.target-of-animation {   animation: my-awesome-beboop 1s, pause-between-iterations 4s; }  @keyframes my-awesome-beboop {   ... }  @keyframes pause-between-iterations {   /* Other animation is visible for 25% of the time */   0% {     opacity: 1;   }   25% {     opacity: 1;   }   /* Other animation is hidden for 75% of the time */   25.1% {     opacity: 0;	   }   100% {     opacity: 0;   } } 

A limitation of this technique is that the pause between animations must be an integer multiple of the “paused” keyframes. That’s because keyframes that repeat infinitely will immediately execute again, even if there are longer running keyframes being applied to the same element.

Interesting aside: When I started this article, I mistakenly thought that an easing function is applied at 0% and ends at 100%.. Turns out that the easing function is applied to each CSS property, starting at the first keyframe where a value is defined and ending at the next keyframe where a value is defined (e.g., an easing curve would be applied from 25% to 75%, if the keyframes were 25% { left: 0 } 75% { left: 50px}). In retrospect, this totally makes sense because it would be hard to adjust your animation if it was a subset of the total easing curve, but my mind is slightly blown.

In the my-awesome-beboop keyframes example above, my-awesome-beboop will run three times behind the scenes during the pause-between-animations keyframes before being revealed for what appears to be its second loop to the user (which is really the fifth time it’s been executed).  

Here’s an example that uses this to add a delay between the shooting stars:


Can’t hide your animation during the delay?

If you need to keep your animation on screen during the delay, there is another option besides hiding. You can still use a second set of @keyframes, but animate a CSS property in a way that counteracts or nullifies the motion of the primary animation. For example, if your main animation uses translateX, you can animate left or margin-left in your set of delay @keyframes.

Here’s a couple of examples:

Pause by changing transform-origin:

Pause by counter-acting transform: translateX by animating the left property:

In the case of the pausing the translateX animation, you’ll need to get fancier with the @keyframes if you need to pause the animation for more than just a single iteration:

/* pausing the animation for three iterations */ @keyframes slide-left-pause {   25%, 50%, 75% {     left: 0;   }   37.5%, 62.5%, 87.5% {     left: -100px;   }   100% {     left: 0;   } }

You may get some slight jitter during the pause. In the translateX example above, there’s some minor vibration on the ball during the slide-left-pause as the animations fight each other for dominance.

Wrap up

The best option performance-wise is to hide the element during the delay or animate transform. Animating properties like left, margin, width are much more intense on a processor than animating opacity (although the contain property appears to be changing that).

If you have any insights or comments on this idea, let me know!

Thanks to Yusuke Nakaya for the original shooting stars CSS animation that I forked on CodePen.

The post A New Way to Delay Keyframes Animations appeared first on CSS-Tricks.


, ,

Design News: 2nd of June Edition


Overlapping Header with CSS Grid

Snook shows off a classic design with an oversized header up top, and a content area that is “pulled up” into that header area. My mind goes to the same place:

Historically, I’ve done this with negative margins. The header has a height that adds a bunch of padding to the bottom and then the body gets a margin-top: -50px or whatever the design calls for.

If you match the margin and padding with a situation like this, it’s not exactly magic numbers, but it still doesn’t feel great to me beaus they’re still numbers you need to keep in sync across totally different elements.

His idea? Build it with CSS grid instead. Definitely feels much more robust.

Random coinsidence, I was reading Chen Hui Jing’s “The one in black and orange” post and the pattern showed up there as well.

Direct Link to ArticlePermalink

The post Overlapping Header with CSS Grid appeared first on CSS-Tricks.


, ,

Rotated Table Column Headers… Now With Fewer Magic Numbers!

Rotated <table> column headers is something that’s been covered before right here on CSS-Tricks, so shout-out to that for getting me started and helping me achieve this effect. As the article points out, if you aren’t using trigonometry to calculate your table styles, you’ll have to rely on magic numbers and your table will be brittle and any dreams of responsiveness crushed. 

Fortunately, in this case, we can take the trigonometry out and replace it with some careful geometry and our magic numbers all turn into 0 (a truly magical number).

For those in a hurry, here is the CSS (it’s very similar to the styles in the other article). Below is a thorough walk-through.

<th class="rotate"><div><span>Column Header 1</span></div></th>
table {  border-collapse: collapse;  --table-border-width: 1px; } th.rotate {   white-space: nowrap;   position: relative; 
} th.rotate > div {   /* place div at bottom left of the th parent */   position: absolute;   bottom: 0;   left: 0;   /* Make sure short labels still meet the corner of the parent otherwise you'll get a gap */   text-align: left;   /* Move the top left corner of the span's bottom-border to line up with the top left corner of the td's border-right border so that the border corners are matched    * Rotate 315 (-45) degrees about matched border corners */   transform:      translate(calc(100% - var(--table-border-width) / 2), var(--table-border-width))     rotate(315deg);   transform-origin: 0% calc(100% - var(--table-border-width));   width: 100%; 
} th.rotate > div > span {   /* make sure the bottom of the span is matched up with the bottom of the parent div */   position: absolute;   bottom: 0;   left: 0;   border-bottom: var(--table-border-width) solid gray; } td {   border-right: var(--table-border-width) solid gray;   /* make sure this is at least as wide as sqrt(2) * height of the tallest letter in your font or the headers will overlap each other*/   min-width: 30px;   padding-top: 2px;   padding-left: 5px;   text-align: right; }

Let’s unpack this table and see what’s going on. The magic starts with that funny chain of HTML tags. We’re putting a <span> inside of a <div> inside of our <th>. Is this all really necessary? Between how borders behave, the positioning flexibility we need, and what determines the width of a table column… yes, they each have a purpose and are necessary.

Let’s see what happens if we rotate the <th> directly:

<th class="rotate">Column header 1</th>
table {   border-collapse: collapse; } th.rotate {   border-bottom: 1px solid gray;   transform: rotate(315deg);   white-space: nowrap; } td {   border-right: 1px solid gray;   min-width: 30px;   padding-top: 2px;   padding-left: 5px;   text-align: right; }

Ignoring the fact that we haven’t corrected position, there are two big issues here: 

  1. The column width is still calculated from the header length which is what we were trying to avoid.
  2. Our border didn’t come with us in the rotation, because it is actually part of the table.

These problems aren’t so difficult to fix. We know that if the <th> has a child element with a border, the browser won’t treat that border as part of the table. Further, we know that absolutely-positioned elements are taken out of the document flow and won’t affect the parent’s width. Enter <div> tag, stage left…and right, I guess.

<th class="rotate"><div>Column header 1</div></th>
table {   border-collapse: collapse; } th.rotate {   white-space: nowrap;   position: relative; } th.rotate > div {   position: absolute;   transform: rotate(315deg);   border-bottom: 1px solid gray; } td {   border-right: 1px solid gray;   min-width: 30px;   text-align: right;   padding-top: 2px;   padding-left: 5px; }
Now our headers don’t influence the column width and the borders are rotated. We just need to line things up.

It’s easier to tell in the image with the rotated <th> elements, but that rotation is happening around the center of the element (that’s the default behavior of transform-origin). It is only another transform in x and y to get it to the right spot, but this is where we’d need trigonometry to figure out just how much x and y to line it up with the column borders. If we instead carefully choose the point to rotate the header about, and use transform-origin to select it, then we can end up with distances that are more straightforward than magic numbers.

The animation below helps illustrate what we’re going to do to avoid complicated math. The black dot in the top left of the blue border needs to match the red dot on the right border of the table column and rotate about it. Then there won’t be any gaps between the two borders.

It’s not helpful to start going somewhere if you don’t know where you are. The absolute positioning is going to help us out with this. By specifying bottom: 0; left: 0; on the <div>, it ends up at the bottom left of the parent <th>. This means the <div> border’s bottom-left corner is sitting on top of the left column border and halfway through it. From here, it’s apparent we need to move down one border width and over one cell width, but how are we going to get that responsively? It’s at this very moment you may recall that we haven’t added the <span> yet — we’re going to need it!

We’ll use the <div> to “figure out” how big the table cells are and the <span> to actually hold the text and position it absolutely as well to overflow the parent.

<th class="rotate"><div><span>Column header 1</span></div></th>
th.rotate{   white-space: nowrap;   position: relative; } th.rotate > div {   position: absolute;   bottom: 0;   left: 0;   width: 100%;  /* <- now the div parent is as wide as the columns */ } th.rotate > div > span {   position: absolute;   bottom: 0;   left: 0;   border-bottom: 1px solid gray; }

Great! When we set the width of the <div> to 100%, it holds the information for how big the column is regardless of what the content is in the table cells. With this in place, we can easily translate things over by the width of the <div> — but don’t forget that we need to shave off a half border width. Our translation becomes:

transform: translate( calc( 100% - var(--table-border-width)/2), var(--table-border-width));

The <div> is now in the right spot to rotate, but we have to make sure to pick the correct transform-origin. We want it to be on the top-left corner of the border, which will be on the left and up one border’s width from the bottom of our <div> element:

transform-origin: 0%, calc(100% - var(--table-border-width));

This brings us to our final style for the table header.

table {   border-collapse: collapse;   --table-border-width: 1px; } th.rotate{   white-space: nowrap;   position: relative; } th.rotate > div {   position: absolute;   bottom: 0;   left: 0;   width: 100%;   transform:     translate( calc( 100% - var(--table-border-width)/2), var(--table-border-width));     rotate(315deg);   transform-origin: 0%, calc(100% - var(--table-border-width)); } th.rotate > div > span {   position: absolute;   bottom: 0;   left: 0;   border-bottom: var(--table-border-width) solid gray; }

Note that transformations happen after everything is placed. That means the rotated headers will overflow onto everything as best they can. You will need to wrap the whole table in something to compensate for the unexpected height. I put the title and table together in a flexbox <div> and set the flex-basis of the title to a value large enough to compensate for the tall headers.

#div-with-table {   display: flex;   flex-direction: column;   justify-content: space-around; } #title {   flex-basis: 140px; }

The post Rotated Table Column Headers… Now With Fewer Magic Numbers! appeared first on CSS-Tricks.


, , , , , ,